三層式的層次劃分
2024-07-21 02:16:11
供稿:網友
 
三層式開發中的層次劃分討論
先舉一個曾經在哪本書上看到的例子:現在你想在1米寬的小溪上建一座橋,你會在上面放塊木板就完了。如果想在寬一點的小河上建這橋,你就需要計算木材用料,價格等,如果需要別人幫忙,你還要多一些圖紙什么的讓別人理解你的想法。現在你要在大江上面建橋,你需要有整體的計劃,包括各個方面,比如將來可能的收費和利益分配等問題。
這里講3層式,其實是針對“大江上面建橋”來的,對于1米寬的小溪,在實際中可能一點用都沒有。不過現在我不可能去拿個長江大橋作例子來講,所以這里還是用這條簡單的小溪,講講怎么建橋。之所以講這么多廢話,是為了防止部分人看完此文之后“小小一個東西,搞那么麻煩干什么。。”其實這里講的不是具體的這個例子,而是分層的思想,理解這點非常重要。
下面我就我們大家日常見最多的例子來講,就是“用戶登錄”的例子。這個例子很簡單,但是麻雀雖小五臟俱全。從數據訪問到業務規則到界面全有了。
本文分2個部分,如果只想研究面向對象的思想,對實現已經熟悉,可以跳過第一部分。
第一部分
 
新建一個空白解決方案。然后:
“添加”-“新建項目”-“其他項目”-“企業級模版項目”-“c#生成塊”-“數據訪問”(數據層,下簡稱d層)
“添加”-“新建項目”-“其他項目”-“企業級模版項目”-“c#生成塊”-“業務規則”(業務層,下簡稱c層)
“添加”-“新建項目”-“其他項目”-“企業級模版項目”-“c#生成塊”-“web用戶界面”(界面層,下簡稱u層)
右鍵點“解決方案”-“項目依賴項”,設置u依賴于d、c,c依賴于d。
對u添加引用d、c,對c添加引用d。
到此為止,一個三層的架子建立起來了。我上面說的很具體很“傻瓜”,知道的人覺得我廢話,其實我這段時間很強烈的感覺到非常多的人其實對這個簡單的過程完全不了解。雖然不反對建2個“空項目”和1個“asp net web應用程序項目”也可以作為3層的框架,而且相當多的人認為其實這些“企業級模板項目”其實就是個空項目,這是一個誤區。沒錯,企業級模板項目你從解決方案資源管理器里看它是個什么也沒有的,但是你可以用記事本打開項目文件,看見不同了吧??有些東西在背后,你是看不見的,不過系統已經做好了。也就是說,如果你在c層里的某個類里“using system data sqlclineit”,或者使用一個sqlconnection對象,編譯時候不會出錯,但是會在“任務列表”里生成一些“策略警告”,警告你在c層里不要放應該放在d層的東西(雖然就程序來說沒錯,但是可讀性可維護性就打了折扣)而這種功能,空項目是無法給你的。
 
我們知道建橋需要磚塊,應該是先準備好磚再來建橋,不過為了講解上的順序性和連貫性,簡單性。我們先建橋,建的過程中需要磚塊再現做,這樣就不會多出來“橋不需要的東西”。注意在實際中,還是應該先準備磚塊。
 
u層其實就是橋,c層是磚塊,d層是原料(石頭、沙子)。這也解釋前面為什么u層要引用、依賴d層(而不是u對c,c對d的層次),因為橋除了需要磚頭,其實也需要石頭沙子。
我們在u層建一個login aspx(這里插入一句,我不喜歡去把系統自動生成的webform1 aspx拿來改成login或index或直接刪除,我一般留著它當測試代碼用,等到整個系統凍結再把它移除就可以了。)添加1個textbox(id=txt),一個dropdownlist(id=ddl),一個button(id=btn)。其中dropdownlist用來選擇用戶名,button是提交按鈕, textbox用來輸入密碼。
現在我們必須要添加的代碼分為2部分: 1、page_load時對ddl的初始化。2、btn的click處理。
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種彈出錯誤。
 
 
注:本文為原創,甚至在寫本文的時候,并沒有看任何網頁文章和書,完全是一時之作,錯誤難免,而且連代碼也是在寫字板上打出來的,所以不見得可以運行,大小寫也可能有錯。一口氣寫這么多,行文很亂,廢話也多,請見諒!