三層式開發中的層次劃分討論

javaprogramers發表於2005-04-21

先舉一個曾經在哪本書上看到的例子:現在你想在1米寬的小溪上建一座橋,你會在上面放塊木板就完了。如果想在寬一點的小河上建這橋,你就需要計算木材用料,價格等,如果需要別人幫忙,你還要多一些圖紙什麼的讓別人理解你的想法。現在你要在大江上面建橋,你需要有整體的計劃,包括各個方面,比如將來可能的收費和利益分配等問題。

這裡講3層式,其實是針對“大江上面建橋”來的,對於1米寬的小溪,在實際中可能一點用都沒有。不過現在我不可能去拿個長江大橋作例子來講,所以這裡還是用這條簡單的小溪,講講怎麼建橋。之所以講這麼多廢話,是為了防止部分人看完此文之後“小小一個東西,搞那麼麻煩幹什麼。。”其實這裡講的不是具體的這個例子,而是分層的思想,理解這點非常重要。

下面我就我們大家日常見最多的例子來講,就是“使用者登入”的例子。這個例子很簡單,但是麻雀雖小五臟俱全。從資料訪問到業務規則到介面全有了。

本文分2個部分,如果只想研究物件導向的思想,對實現已經熟悉,可以跳過第一部分。

第一部分

 

新建一個空白解決方案。然後:

“新增”-“新建專案”-“其他專案”-“企業級模版專案”-“C#生成塊”-“資料訪問”(資料層,下簡稱D層)

“新增”-“新建專案”-“其他專案”-“企業級模版專案”-“C#生成塊”-“業務規則”(業務層,下簡稱C層)

“新增”-“新建專案”-“其他專案”-“企業級模版專案”-“C#生成塊”-“Web使用者介面”(介面層,下簡稱U層)

右鍵點“解決方案”-“專案依賴項”,設定U依賴於DCC依賴於D

U新增引用DC,對C新增引用D

到此為止,一個三層的架子建立起來了。我上面說的很具體很“傻瓜”,知道的人覺得我廢話,其實我這段時間很強烈的感覺到非常多的人其實對這個簡單的過程完全不瞭解。雖然不反對建2個“空專案”和1個“Asp net Web應用程式專案”也可以作為3層的框架,而且相當多的人認為其實這些“企業級模板專案”其實就是個空專案,這是一個誤區。沒錯,企業級模板專案你從解決方案資源管理器裡看它是個什麼也沒有的,但是你可以用記事本開啟專案檔案,看見不同了吧??有些東西在背後,你是看不見的,不過系統已經做好了。也就是說,如果你在C層裡的某個類裡“using System Data SqlClineit”,或者使用一個SqlConnection物件,編譯時候不會出錯,但是會在“任務列表”裡生成一些“策略警告”,警告你在C層裡不要放應該放在D層的東西(雖然就程式來說沒錯,但是可讀性可維護性就打了折扣)而這種功能,空專案是無法給你的。

 

我們知道建橋需要磚塊,應該是先準備好磚再來建橋,不過為了講解上的順序性和連貫性,簡單性。我們先建橋,建的過程中需要磚塊再現做,這樣就不會多出來“橋不需要的東西”。注意在實際中,還是應該先準備磚塊。

 

U層其實就是橋,C層是磚塊,D層是原料(石頭、沙子)。這也解釋前面為什麼U層要引用、依賴D層(而不是UCCD的層次),因為橋除了需要磚頭,其實也需要石頭沙子。

我們在U層建一個Login aspx(這裡插入一句,我不喜歡去把系統自動生成的WebForm1 aspx拿來改成loginindex或直接刪除,我一般留著它當測試程式碼用,等到整個系統凍結再把它移除就可以了。)新增1TextBoxid=txt),一個DropDownListid=ddl),一個Button(id=btn)。其中DropDownList用來選擇使用者名稱,button是提交按鈕, TextBox用來輸入密碼。

現在我們必須要新增的程式碼分為2部分: 1Page_load時對ddl的初始化。2btnclick處理。

1

private void Page_Load(object sender, System.EventArgs e)

{

if(!IsPostBack)

{

this.ddl.DataSourse=DataManager.GetOneColunm(User,uid); //講解1

this.ddl.DataBind();

}

}

2:

private void Btn_Click(object sender, System.EventArgs e)

{

string uid=this.ddl.SelectedValue;

string psw=this.txt.Text;

if(psw =””)

     MessageBox(空密碼!);

else

{

     User theUser;

     try

     {

          theUser=new User(uid); //講解2

     }

     catch(Exception e)

     {

  MessageBox(e. Message);//講解2

  return;

}

if(theUser.CheckPsw(psw)) //講解3

{

  theUser.SetSessions();

  Response.Redirect(“……………..);  //登入成功! 

}

else

{

  MessageBox(密碼錯誤!);

}

}

}

 

 

講解1:DataManager D層中的一個類,提供常見的資料操作。GetOneColunm(string Table,string Colunm)方法返回一個只有1列的DataTable,值為資料庫中表名為Table,的Colunm列。

public class DataManager

{

public DataManager()

{

}

public static DataTable GetOneColunm(string Table,string Colunm)

{

     //此處省略相關程式碼。返回指定表指定列

}

}

其實這個地方演示的是在U層直接繞過C層訪問D層的例子,因為該結構邏輯上很簡單,而且獲取使用者名稱並不是現實社會中的業務邏輯的一部分(僅僅是介面需要,因為在這裡其實用成2個TextBox的話完全不需要這一步)

講解2:定義一個User類的例項。User類的定義可能如下:

public class User

{

public User(string uid)

{

     if(DataManager.IsIn(user,uid="+uid+”’”))

          throw "使用者不存在";

     else
              //User()其他初始化;

}

public bool CheckPsw(string psw)

{

     if(DataManager.IsIn(user,uid="+uid+”’ and psw=’”+psw+”’”))

          return true;

     else

          return false;

}

}

注意到使用者類建構函式中用了個throw來丟擲使用者不存在的異常,在下面catch的時候用MessageBox(e. Message);來彈出“使用者不存在”的錯誤。這裡其實也是為了演示一個層間傳遞資訊的手段,異常也是一種手段,雖然在這裡其實可以有其他方式比如返回值,引用引數之類的直接用一個方法來獲得使用者是否存在的資訊,沒必要放在構造裡,我這麼做只是為了演示傳遞過程,在後面的有討論這種用法在分層模式下某種特殊情況的應用以解決一些問題。這個類裡又用了DataManager類的一個靜態方法IsIn(string Table,string str)該方法其實其實是執行 select * from Table where str

這個Sql語句並在返回空的時候方法返回false,否則返回true。一個很簡單的方法。這裡演示了C層對D層的呼叫。

 

順便說一句,因為在VS.Net中,專案的名稱會預設地成為專案中的namespace,可以通過把所有自動生成的程式碼中的namespace改為“解決方案名稱”來使3個層可以無縫地自由呼叫。或者在呼叫的地方using一下其他層的空間名,看個人喜歡了。比如上面的Login.aspx.cs裡需要using2個,而User.cs裡要using一個。

講解3:這裡的檢查使用者密碼同樣用到User類的一個方法CheckPsw()而這個方法 又用到了IsIn()這裡就不多說了。

大家注意到我們在U層的頁面裡用MessageBox()方法來彈出對話方塊,其實這個方法寫在PageBase.cs裡,是U層的另外一個檔案,繼承Page類,Login類又繼承它,這個方法其實是把Response.Write(<script>alert(/“”+ msg+/)</script>)封裝起來了。

 

 

到此為止,登入結束,例子的實現也說完了。不過只講了“然”,沒有講“所以然”。下面開始講“所以然”。

第二部分

 

作為對比,我們使用一個不物件導向的,不分層的Asp式的Aspx相同登入作為對比。具體的Asp程式碼我就不寫了,反正登入哪都有。先來看看他們2者發生的遭遇(這是不幸的,卻偏偏是經常發生的):

1、  專案經理突然說“不用SqlServer了,換成Access”(正版費用問題)。看看2邊分別發生什麼:3層這邊(A),把DataManager類裡的連線改改(在實際情況下,極可能其實是改它的基類,它本身不用改),Web.config中把字串換掉就完了。Asp式那邊(B),同樣要改Web.config,同樣要改連線什麼的,修改量在這個具體的“小溪”例子上幾乎相同,在“大橋”例子上B應該會稍微多改點,不過也不會多很多。但是!請注意一點,我們在修改程式碼的時候,主要時間和精力不是花在“改”這個動作上,而是花在“要改什麼地方”上和“尋找需要改的地方”上。在“大橋”上,B需要花費多的多的時間,對大部分檔案進行查詢和替換。A則僅僅在資料層裡,另外2個層不需要任何修改。從這個角度出發我們想到2點原則:

a)    資料層必須要能夠保證資料庫的變動(任何結構變動、型別變動)對其餘各層的不透明性。也就是資料庫怎麼變,其他層絕對不應該變哪怕1行程式碼!(web.config是整個應用程式的配置,雖然在物理上存在於U層的資料夾中,但個人更願意認為它是獨立的不屬於任何層的,所以這裡不計它)

b)    資料層越小越好(如果沒有這點原則,我們把整個所有的東西都放在資料層,那當然資料庫變動對外面無影響――因為外面幾乎沒東西――但是這顯然不可行)。而且因為前面我們說了,大部分時間花在“找”上面,你小點,找起來也容易點。

2、  客戶突然提出B/S版的不好,要換成C/S版的。對於(B)來說,這是晴天霹靂!!他的所有工作都要重新做,(或者幾乎所有工作),雖然他有很多程式碼還可以用,不過他在未來一小段時間就必須不斷在“複製-貼上”中使用以前的程式碼。(A)發生了什麼??如果你細心看會發現(A)之需要新建個專案“Windows使用者介面”(和前面一樣,新增引用,專案依賴),拖幾個控制元件到上面,把控制元件名字起成txt,ddl,btn,然後把click程式碼和Pageload程式碼複製過去,(居然。。。)連1行程式碼都不需要修改!!!!當然,這是比較極端的例子(win和web都有TextBox,DropDownList,Button3種控制元件,而且我們在PageBase裡定義的方法MessageBox()又剛好和win裡面方法同名。。。)不過儘管有這麼多巧合我們仍然可以也願意相信,在“大橋”上,(A)將比(B)少做很多工作。從這個角度出發我們又想到2點類似原則:

a)    介面層應該保證介面的任何變化都不需要修改其他層的內容(不管這個具體的例子把ddl改為另外一個TextBox,或是把B/S改為C/S)

b)    介面層越小越好(理由同上。)

3、  除開了介面層和資料層,(如果你的方案中只有3個層的話)剩下的就都是邏輯層的內容了。所以和前面的相對應,我們可以得出結論:

a)    邏輯層應當不受資料庫和介面變動的影響而需要修改。

b)    邏輯層越大越好(因為另外2層越小越好。。。)

 

有了最基本的原則,我們應該來討論下,根據原則,要怎麼分層的問題:

1、  PageBase.cs 應該放在哪個層?根據上面的原則,應該放在C層。但是實際上我習慣放在U層,或者放在另外一個(第4個層,通用底層,在比資料層還低的位置)層裡。到底放在什麼地方,我最開始的做法是在C層,因為按上面歸納的原則,就應該放在C,但是後來一段時間我習慣於“四層式”之後就把它放在通用底層(下簡稱B層,該層同時也放如本來在D層中的SqlHelper類等,包括原來3層中所有“通用”的類,這裡通用的意思是說其他系統也可以用的到而不需要修改,這個層通常不用解決方案名稱而用公司、小組名稱等作為namespace,在有新專案的時候在建解決方案的時候就可以“新增現有專案”,簡單的加進去並不斷積累,實踐中對提高效率和程式碼重用有比較大作用。)不過如果只有3層,我現在傾向於把PageBase放在U層。主要因為最近一段潛心研究物件導向的分析設計的心得。說起來又是一大匹布沒完,不過我又在前面的“原則”上加1條:“如果某個類,僅為了某層的某種特殊實現而存在,那麼它必須放在該層”,比如PageBase是為了U層的特殊實現(B/S實現)而存在,又比如SqlHelper是為了D層的特殊實現(SqlServer資料庫)而存在。所以對應的,它們必須分別放在U層和D層(如果不加這條的話按前面他們都該放在C層,因為C層越大越好,而且資料庫和介面的變動不需要改動這2個類-雖然它們可能因改動而沒有用了,不過還是不需要去修改它們)

2、  Oldjacky曾經和我談到一個問題:Datagrid中允許作刪除操作,但是如果當前僅餘下最後一條記錄,則不允許這個刪除操作!那麼該刪除應該放在C層還是D層還是U層?我覺得應該從另外一個角度來考慮:

a)    這種“不允許”是“業務規則的不允許”(比如表內的資料表示當前在店裡的職員,刪除表示職員離開店裡-可能去拿貨什麼的,新增表示職員回來,當櫃檯只有一名職員時,顯然他絕對不能離開去送貨),這個時候,此“禁止刪除”的操作應該產生在C層。

b)    這種“不允許”是“程式實現的不允許”(比如當這裡為空的時候會引起其他地方比如ToString()方法產生“未將物件的引用設定到物件的例項……”的錯誤,或程式設計者或專案經理的主觀願望希望它“不允許”以此來減少工作量或簡化程式)。這個時候,此“禁止刪除”可以放在U層(比如上面說的ToString)或D層(比如違反資料庫約束)

3、  細心的人可能會發現,前面的登入例子裡,使用者一共可以獲得3種彈出錯誤分別是“空密碼”“密碼錯誤”“使用者不存在”,而其中前2個是在U層裡做的,“使用者不存在”卻是在C層裡做的(我是指這個字串)還是開始說的建橋,我這裡是用“小溪建橋”來講解“大江建橋”所以故意在這裡轉了個沒用的圈,就像在計算小溪上這塊木板到底夠用多少年,其實對小溪沒什麼意義,只是為了講解大橋需要而加上去的,畢竟大橋需要這種考慮。我這裡假設“使用者不存在需要彈出提示”是一種業務邏輯上的需要,而“未輸入密碼需要提示”則不是業務規則需要(比如實際業務中可以允許空密碼,但是專案經理不同意,說一定要密碼)在這個登入例子中其實根本沒有什麼問題,但是在大專案裡,如果這個東西不是業務規則的需要,就不應該放在業務層,如果是一種業務規則,就要放在業務層。有助於業務模型與現實實體的銜接,也有益於業務邏輯更好地表現現實實體的特徵。

 

到此為止,我再次歸納出我們的最終的原則:

1、  如果某個類,僅為了某層的某種特殊實現而存在,那麼它必須放在該層。

2、  資料層應當在保證資料庫變化對其他層不可見的前提下儘量小。

3、  介面層應當在保證介面變化對業務邏輯層不影響的前提下儘量小。

4、  如果某個類不是業務規則的需要,就不應該放在業務層,反之亦然。

5、  邏輯層應當在保證資料庫或介面變化不會造成自身影響的前提下儘量大。

 

以上5點如果發生衝突,在找平衡點的時候,前面的要高於後面的。比如1和3衝突的時候更傾向於使用規則1。

第二部分結束

 

有一點應該是“程式設計程式碼習慣”和“物件導向”的範疇,不過因為和分層有些關係,所以也說一下。“如果你的程式碼,自己把它翻譯成中文並加必要的標點符號後,其他不懂程式的人看了仍然覺得很亂,那麼你很可能層沒分好”。比如前面的btn的click:

字串 使用者名稱是 下拉框 選擇值;

字串 密碼是 輸入框 值;

如果 密碼是 空

   對話方塊(密碼空!);

否則

   使用者 這使用者;

   嘗試

這使用者 是 新的 使用者(使用者名稱);

捕捉(錯誤)

對話方塊(錯誤 訊息);

返回;

如果 這使用者檢查密碼(密碼)

{

這使用者 設定狀態;

響應 重定位(“。。。。。”);

}

否則

{

對話方塊(密碼錯誤)

}

程式碼最好能讓不懂的人也能看懂到底在幹什麼。

 

最後,oldjacky的Datagrid刪除的例子“刪除”顯然在D層,但是不允許卻可能在C或U,如果在U沒什麼說的了,如果在C,那麼這種“不允許”的一個比較合理的實現方法就是在C層裡遇到這種情況throw一下。當U層裡catch到該throw的時候,禁止刪除操作,這樣當2個層同時有原因引起禁止時,可以從程式碼一眼看出這種禁止的來源。類似於前面的2種彈出錯誤。

 

相關文章