凯锐

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::
 

先舉一個曾經在哪本書上看到的例子:現在你想在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("
密碼錯誤!");  

}  


}  

}  


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

public  class  DataManager  

{  

public  DataManager()  

{  

}  

blic  static  DataTable  GetOneColunm(string  Table,string  Colunm)  

{  

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


}  


}  


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


講解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("")封裝起來了。  








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


第二部分  




作為對比,我們使用一個不面向物件的,不分層的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用戶介面"(和前面一樣,添加引用,專案依賴),拖幾個控制項到上面,把控制項名字起成txtddlbtn,然後把click代碼和Pageload代碼複製過去,(居然。。。)連1行代碼都不需要修改!!!!當然,這是比較極端的例子(winweb都有TextBoxdropDownListButton3種控制項,而且我們在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點如果發生衝突,在找平衡點的時候,前面的要高於後面的。比如13衝突的時候更傾向於使用規則1  


第二部分結束  





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


  


字串  用戶名是  下拉清單  選擇值;  


字串  密碼是  輸入框  值;  


如果  密碼是    


對話方塊(密碼空!);  


否則  


  


用戶  這用戶;  


嘗試  


  


這用戶    新的  用戶(用戶名);  


  


捕捉(錯誤)  


  


對話方塊(錯誤  消息);  


返回;  


  


如果  這用戶檢查密碼(密碼)  


{  


這用戶  設置狀態;  


回應  重定位("。。。。。");  


}  


否則  


{  


對話方塊(密碼錯誤)  


}  


  


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


最後,oldjackyDatagrid刪除的例子"刪除"顯然在D層,但是不允許卻可能在CU,如果在U沒什麼說的了,如果在C,那麼這種"不允許"的一個比較合理的實現方法就是在C層裏遇到這種情況throw一下。當U層裏catch到該throw的時候,股境僮鰨?個層同時有原因引起禁止時,可以從代碼一眼看出這種禁止的來源。類似於前面的2種彈出錯誤。  


注:本文為原創,甚至在寫本文的時候,並沒有看任何網頁文章和書,完全是一時之作,錯誤難免,而且連代碼也是在寫字板上打出來的,所以不見得可以運行,大小寫也可能有錯。一口氣寫這麼多,行文很亂,廢話也多,請見諒!


作者Bloghttp://blog.csdn.net/syeerzy/

posted on 2006-05-05 09:58  凯锐  阅读(1194)  评论(2编辑  收藏  举报