[转] 三层开发中的层次划分
生活随笔
收集整理的這篇文章主要介紹了
[转] 三层开发中的层次划分
小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.
??? 先舉一個(gè)曾經(jīng)在哪本書上看到的例子:現(xiàn)在你想在1米寬的小溪上建一座橋,你會(huì)在上面放塊木板就完了。如果想在寬一點(diǎn)的小河上建這橋,你就需要計(jì)算木材用料,價(jià)格等,如果需要?jiǎng)e人幫忙,你還要多一些圖紙什么的讓別人理解你的想法。現(xiàn)在你要在大江上面建橋,你需要有整體的計(jì)劃,包括各個(gè)方面,比如將來可能的收費(fèi)和利益分配等問題。
這里講3層式,其實(shí)是針對(duì)“大江上面建橋”來的,對(duì)于1米寬的小溪,在實(shí)際中可能一點(diǎn)用都沒有。不過現(xiàn)在我不可能去拿個(gè)長江大橋作例子來講,所以這里還是用這條簡單的小溪,講講怎么建橋。之所以講這么多廢話,是為了防止部分人看完此文之后“小小一個(gè)東西,搞那么麻煩干什么。。”其實(shí)這里講的不是具體的這個(gè)例子,而是分層的思想,理解這點(diǎn)非常重要。
下面我就我們大家日常見最多的例子來講,就是“用戶登錄”的例子。這個(gè)例子很簡單,但是麻雀雖小五臟俱全。從數(shù)據(jù)訪問到業(yè)務(wù)規(guī)則到界面全有了。
本文分2個(gè)部分,如果只想研究面向?qū)ο蟮乃枷?#xff0c;對(duì)實(shí)現(xiàn)已經(jīng)熟悉,可以跳過第一部分。
第一部分
新建一個(gè)空白解決方案。
然后:
①“添加”-“新建項(xiàng)目”-“其他項(xiàng)目”-“企業(yè)級(jí)模版項(xiàng)目”-“C#生成塊”-“數(shù)據(jù)訪問”(數(shù)據(jù)層,下簡稱D層)
②“添加”-“新建項(xiàng)目”-“其他項(xiàng)目”-“企業(yè)級(jí)模版項(xiàng)目”-“C#生成塊”-“業(yè)務(wù)規(guī)則”(業(yè)務(wù)層,下簡稱C層)
③“添加”-“新建項(xiàng)目”-“其他項(xiàng)目”-“企業(yè)級(jí)模版項(xiàng)目”-“C#生成塊”-“Web用戶界面”(界面層,下簡稱U層)
④右鍵點(diǎn)“解決方案”-“項(xiàng)目依賴項(xiàng)”,設(shè)置U依賴于D、C,C依賴于D。
⑤對(duì)U添加引用D、C,對(duì)C添加引用D。
到此為止,一個(gè)三層的架子建立起來了。
我上面說的很具體很“傻瓜”,知道的人覺得我廢話,其實(shí)我這段時(shí)間很強(qiáng)烈的感覺到非常多的人其實(shí)對(duì)這個(gè)簡單的過程完全不了解。雖然不反對(duì)建2個(gè)“空項(xiàng)目”和1個(gè)“Asp net Web應(yīng)用程序項(xiàng)目”也可以作為3層的框架,而且相當(dāng)多的人認(rèn)為其實(shí)這些“企業(yè)級(jí)模板項(xiàng)目”其實(shí)就是個(gè)空項(xiàng)目,這是一個(gè)誤區(qū)。沒錯(cuò),企業(yè)級(jí)模板項(xiàng)目你從解決方案資源管理器里看它是個(gè)什么也沒有的,但是你可以用記事本打開項(xiàng)目文件,看見不同了吧??有些東西在背后,你是看不見的,不過系統(tǒng)已經(jīng)做好了。也就是說,如果你在C層里的某個(gè)類里“using System Data SqlClineit”,或者使用一個(gè)SqlConnection對(duì)象,編譯時(shí)候不會(huì)出錯(cuò),但是會(huì)在“任務(wù)列表”里生成一些“策略警告”,警告你在C層里不要放應(yīng)該放在D層的東西(雖然就程序來說沒錯(cuò),但是可讀性可維護(hù)性就打了折扣)而這種功能,空項(xiàng)目是無法給你的。
我們知道建橋需要磚塊,應(yīng)該是先準(zhǔn)備好磚再來建橋,不過為了講解上的順序性和連貫性,簡單性。我們先建橋,建的過程中需要磚塊再現(xiàn)做,這樣就不會(huì)多出來“橋不需要的東西”。注意在實(shí)際中,還是應(yīng)該先準(zhǔn)備磚塊。
U層其實(shí)就是橋,C層是磚塊,D層是原料(石頭、沙子)。這也解釋前面為什么U層要引用、依賴D層(而不是U對(duì)C,C對(duì)D的層次),因?yàn)闃虺诵枰u頭,其實(shí)也需要石頭沙子。
我們?cè)赨層建一個(gè)Login aspx(這里插入一句,我不喜歡去把系統(tǒng)自動(dòng)生成的WebForm1 aspx拿來改成login或index或直接刪除,我一般留著它當(dāng)測試代碼用,等到整個(gè)系統(tǒng)凍結(jié)再把它移除就可以了。)添加1個(gè)TextBox(id=txt),一個(gè)DropDownList(id=ddl),一個(gè)Button(id=btn)。其中DropDownList用來選擇用戶名,button是提交按鈕, TextBox用來輸入密碼。
現(xiàn)在我們必須要添加的代碼分為2部分:
1、Page_load時(shí)對(duì)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(“密碼錯(cuò)誤!”);??
????????}?
?????}?
}?
講解1:DataManager 是D層中的一個(gè)類,提供常見的數(shù)據(jù)操作。GetOneColunm(string Table,string Colunm)方法返回一個(gè)只有1列的DataTable,值為數(shù)據(jù)庫中表名為Table,的Colunm列。 public?class?DataManager?
{?
????public?DataManager()?
?????{?
?????}?
?????public?static?DataTable?GetOneColunm(string?Table,string?Colunm)?
?????{?
??????????//此處省略相關(guān)代碼。返回指定表指定列?
?????}?
}?
其實(shí)這個(gè)地方演示的是在U層直接繞過C層訪問D層的例子,因?yàn)樵摻Y(jié)構(gòu)邏輯上很簡單,而且獲取用戶名并不是現(xiàn)實(shí)社會(huì)中的業(yè)務(wù)邏輯的一部分(僅僅是界面需要,因?yàn)樵谶@里其實(shí)用成2個(gè)TextBox的話完全不需要這一步)
講解2:定義一個(gè)User類的實(shí)例。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;?
?????}?
}?
注意到用戶類構(gòu)造函數(shù)中用了個(gè)throw來拋出用戶不存在的異常,在下面catch的時(shí)候用MessageBox(e. Message);來彈出“用戶不存在”的錯(cuò)誤。這里其實(shí)也是為了演示一個(gè)層間傳遞信息的手段,異常也是一種手段,雖然在這里其實(shí)可以有其他方式比如返回值,引用參數(shù)之類的直接用一個(gè)方法來獲得用戶是否存在的信息,沒必要放在構(gòu)造里,我這么做只是為了演示傳遞過程,在后面的有討論這種用法在分層模式下某種特殊情況的應(yīng)用以解決一些問題。這個(gè)類里又用了DataManager類的一個(gè)靜態(tài)方法IsIn(string Table,string str)該方法其實(shí)其實(shí)是執(zhí)行 “select * from Table where str”
這個(gè)Sql語句并在返回空的時(shí)候方法返回false,否則返回true。一個(gè)很簡單的方法。這里演示了C層對(duì)D層的調(diào)用。
順便說一句,因?yàn)樵赩S.Net中,項(xiàng)目的名稱會(huì)默認(rèn)地成為項(xiàng)目中的namespace,可以通過把所有自動(dòng)生成的代碼中的namespace改為“解決方案名稱”來使3個(gè)層可以無縫地自由調(diào)用。或者在調(diào)用的地方using一下其他層的空間名,看個(gè)人喜歡了。比如上面的Login.aspx.cs里需要using2個(gè),而User.cs里要using一個(gè)。
講解3:這里的檢查用戶密碼同樣用到User類的一個(gè)方法CheckPsw()而這個(gè)方法 又用到了IsIn()這里就不多說了。
大家注意到我們?cè)赨層的頁面里用MessageBox()方法來彈出對(duì)話框,其實(shí)這個(gè)方法寫在PageBase.cs里,是U層的另外一個(gè)文件,繼承Page類,Login類又繼承它,這個(gè)方法其實(shí)是把Response.Write(“<script>alert(\“”+ msg+“\”)</script>”)封裝起來了。
到此為止,登錄結(jié)束,例子的實(shí)現(xiàn)也說完了。不過只講了“然”,沒有講“所以然”。下面開始講“所以然”。
第二部分
作為對(duì)比,我們使用一個(gè)不面向?qū)ο蟮?#xff0c;不分層的Asp式的Aspx相同登錄作為對(duì)比。具體的Asp代碼我就不寫了,反正登錄哪都有。先來看看他們2者發(fā)生的遭遇(這是不幸的,卻偏偏是經(jīng)常發(fā)生的):
1、? 項(xiàng)目經(jīng)理突然說“不用SqlServer了,換成Access”(正版費(fèi)用問題)。看看2邊分別發(fā)生什么:3層這邊(A),把DataManager類里的連接改改(在實(shí)際情況下,極可能其實(shí)是改它的基類,它本身不用改),Web.config中把字符串換掉就完了。Asp式那邊(B),同樣要改Web.config,同樣要改連接什么的,修改量在這個(gè)具體的“小溪”例子上幾乎相同,在“大橋”例子上B應(yīng)該會(huì)稍微多改點(diǎn),不過也不會(huì)多很多。但是!請(qǐng)注意一點(diǎn),我們?cè)谛薷拇a的時(shí)候,主要時(shí)間和精力不是花在“改”這個(gè)動(dòng)作上,而是花在“要改什么地方”上和“尋找需要改的地方”上。在“大橋”上,B需要花費(fèi)多的多的時(shí)間,對(duì)大部分文件進(jìn)行查找和替換。A則僅僅在數(shù)據(jù)層里,另外2個(gè)層不需要任何修改。從這個(gè)角度出發(fā)我們想到2點(diǎn)原則:
a)??? 數(shù)據(jù)層必須要能夠保證數(shù)據(jù)庫的變動(dòng)(任何結(jié)構(gòu)變動(dòng)、類型變動(dòng))對(duì)其余各層的不透明性。也就是數(shù)據(jù)庫怎么變,其他層絕對(duì)不應(yīng)該變哪怕1行代碼!(web.config是整個(gè)應(yīng)用程序的配置,雖然在物理上存在于U層的文件夾中,但個(gè)人更愿意認(rèn)為它是獨(dú)立的不屬于任何層的,所以這里不計(jì)它)
b)??? 數(shù)據(jù)層越小越好(如果沒有這點(diǎn)原則,我們把整個(gè)所有的東西都放在數(shù)據(jù)層,那當(dāng)然數(shù)據(jù)庫變動(dòng)對(duì)外面無影響――因?yàn)橥饷鎺缀鯖]東西――但是這顯然不可行)。而且因?yàn)榍懊嫖覀冋f了,大部分時(shí)間花在“找”上面,你小點(diǎn),找起來也容易點(diǎn)。
2、? 客戶突然提出B/S版的不好,要換成C/S版的。對(duì)于(B)來說,這是晴天霹靂!!他的所有工作都要重新做,(或者幾乎所有工作),雖然他有很多代碼還可以用,不過他在未來一小段時(shí)間就必須不斷在“復(fù)制-粘貼”中使用以前的代碼。(A)發(fā)生了什么??如果你細(xì)心看會(huì)發(fā)現(xiàn)(A)之需要新建個(gè)項(xiàng)目“Windows用戶界面”(和前面一樣,添加引用,項(xiàng)目依賴),拖幾個(gè)控件到上面,把控件名字起成txt,ddl,btn,然后把click代碼和Pageload代碼復(fù)制過去,(居然。。。)連1行代碼都不需要修改!!!!當(dāng)然,這是比較極端的例子(win和web都有TextBox,DropDownList,Button3種控件,而且我們?cè)赑ageBase里定義的方法MessageBox()又剛好和win里面方法同名。。。)不過盡管有這么多巧合我們?nèi)匀豢梢砸苍敢庀嘈?#xff0c;在“大橋”上,(A)將比(B)少做很多工作。從這個(gè)角度出發(fā)我們又想到2點(diǎn)類似原則:
a)??? 界面層應(yīng)該保證界面的任何變化都不需要修改其他層的內(nèi)容(不管這個(gè)具體的例子把ddl改為另外一個(gè)TextBox,或是把B/S改為C/S)
b)??? 界面層越小越好(理由同上。)
3、? 除開了界面層和數(shù)據(jù)層,(如果你的方案中只有3個(gè)層的話)剩下的就都是邏輯層的內(nèi)容了。所以和前面的相對(duì)應(yīng),我們可以得出結(jié)論:
a)??? 邏輯層應(yīng)當(dāng)不受數(shù)據(jù)庫和界面變動(dòng)的影響而需要修改。
b)??? 邏輯層越大越好(因?yàn)榱硗?層越小越好。。。)
有了最基本的原則,我們應(yīng)該來討論下,根據(jù)原則,要怎么分層的問題:
1、? PageBase.cs 應(yīng)該放在哪個(gè)層?根據(jù)上面的原則,應(yīng)該放在C層。但是實(shí)際上我習(xí)慣放在U層,或者放在另外一個(gè)(第4個(gè)層,通用底層,在比數(shù)據(jù)層還低的位置)層里。到底放在什么地方,我最開始的做法是在C層,因?yàn)榘瓷厦鏆w納的原則,就應(yīng)該放在C,但是后來一段時(shí)間我習(xí)慣于“四層式”之后就把它放在通用底層(下簡稱B層,該層同時(shí)也放如本來在D層中的SqlHelper類等,包括原來3層中所有“通用”的類,這里通用的意思是說其他系統(tǒng)也可以用的到而不需要修改,這個(gè)層通常不用解決方案名稱而用公司、小組名稱等作為namespace,在有新項(xiàng)目的時(shí)候在建解決方案的時(shí)候就可以“添加現(xiàn)有項(xiàng)目”,簡單的加進(jìn)去并不斷積累,實(shí)踐中對(duì)提高效率和代碼重用有比較大作用。)不過如果只有3層,我現(xiàn)在傾向于把PageBase放在U層。主要因?yàn)樽罱欢螡撔难芯棵嫦驅(qū)ο蟮姆治鲈O(shè)計(jì)的心得。說起來又是一大匹布沒完,不過我又在前面的“原則”上加1條:“如果某個(gè)類,僅為了某層的某種特殊實(shí)現(xiàn)而存在,那么它必須放在該層”,比如PageBase是為了U層的特殊實(shí)現(xiàn)(B/S實(shí)現(xiàn))而存在,又比如SqlHelper是為了D層的特殊實(shí)現(xiàn)(SqlServer數(shù)據(jù)庫)而存在。所以對(duì)應(yīng)的,它們必須分別放在U層和D層(如果不加這條的話按前面他們都該放在C層,因?yàn)镃層越大越好,而且數(shù)據(jù)庫和界面的變動(dòng)不需要改動(dòng)這2個(gè)類-雖然它們可能因改動(dòng)而沒有用了,不過還是不需要去修改它們)
2、? Oldjacky曾經(jīng)和我談到一個(gè)問題:Datagrid中允許作刪除操作,但是如果當(dāng)前僅余下最后一條記錄,則不允許這個(gè)刪除操作!那么該刪除應(yīng)該放在C層還是D層還是U層?我覺得應(yīng)該從另外一個(gè)角度來考慮:
a)??? 這種“不允許”是“業(yè)務(wù)規(guī)則的不允許”(比如表內(nèi)的數(shù)據(jù)表示當(dāng)前在店里的職員,刪除表示職員離開店里-可能去拿貨什么的,添加表示職員回來,當(dāng)柜臺(tái)只有一名職員時(shí),顯然他絕對(duì)不能離開去送貨),這個(gè)時(shí)候,此“禁止刪除”的操作應(yīng)該產(chǎn)生在C層。
b)??? 這種“不允許”是“程序?qū)崿F(xiàn)的不允許”(比如當(dāng)這里為空的時(shí)候會(huì)引起其他地方比如ToString()方法產(chǎn)生“未將對(duì)象的引用設(shè)置到對(duì)象的實(shí)例……”的錯(cuò)誤,或程序設(shè)計(jì)者或項(xiàng)目經(jīng)理的主觀愿望希望它“不允許”以此來減少工作量或簡化程序)。這個(gè)時(shí)候,此“禁止刪除”可以放在U層(比如上面說的ToString)或D層(比如違反數(shù)據(jù)庫約束)
3、? 細(xì)心的人可能會(huì)發(fā)現(xiàn),前面的登錄例子里,用戶一共可以獲得3種彈出錯(cuò)誤分別是“空密碼”“密碼錯(cuò)誤”“用戶不存在”,而其中前2個(gè)是在U層里做的,“用戶不存在”卻是在C層里做的(我是指這個(gè)字符串)還是開始說的建橋,我這里是用“小溪建橋”來講解“大江建橋”所以故意在這里轉(zhuǎn)了個(gè)沒用的圈,就像在計(jì)算小溪上這塊木板到底夠用多少年,其實(shí)對(duì)小溪沒什么意義,只是為了講解大橋需要而加上去的,畢竟大橋需要這種考慮。我這里假設(shè)“用戶不存在需要彈出提示”是一種業(yè)務(wù)邏輯上的需要,而“未輸入密碼需要提示”則不是業(yè)務(wù)規(guī)則需要(比如實(shí)際業(yè)務(wù)中可以允許空密碼,但是項(xiàng)目經(jīng)理不同意,說一定要密碼)在這個(gè)登錄例子中其實(shí)根本沒有什么問題,但是在大項(xiàng)目里,如果這個(gè)東西不是業(yè)務(wù)規(guī)則的需要,就不應(yīng)該放在業(yè)務(wù)層,如果是一種業(yè)務(wù)規(guī)則,就要放在業(yè)務(wù)層。有助于業(yè)務(wù)模型與現(xiàn)實(shí)實(shí)體的銜接,也有益于業(yè)務(wù)邏輯更好地表現(xiàn)現(xiàn)實(shí)實(shí)體的特征。
到此為止,我再次歸納出我們的最終的原則:
1、? 如果某個(gè)類,僅為了某層的某種特殊實(shí)現(xiàn)而存在,那么它必須放在該層。
2、? 數(shù)據(jù)層應(yīng)當(dāng)在保證數(shù)據(jù)庫變化對(duì)其他層不可見的前提下盡量小。
3、? 界面層應(yīng)當(dāng)在保證界面變化對(duì)業(yè)務(wù)邏輯層不影響的前提下盡量小。
4、? 如果某個(gè)類不是業(yè)務(wù)規(guī)則的需要,就不應(yīng)該放在業(yè)務(wù)層,反之亦然。
5、? 邏輯層應(yīng)當(dāng)在保證數(shù)據(jù)庫或界面變化不會(huì)造成自身影響的前提下盡量大。
以上5點(diǎn)如果發(fā)生沖突,在找平衡點(diǎn)的時(shí)候,前面的要高于后面的。比如1和3沖突的時(shí)候更傾向于使用規(guī)則1。
第二部分結(jié)束
有一點(diǎn)應(yīng)該是“編程代碼習(xí)慣”和“面向?qū)ο蟆钡姆懂?#xff0c;不過因?yàn)楹头謱佑行╆P(guān)系,所以也說一下。“如果你的代碼,自己把它翻譯成中文并加必要的標(biāo)點(diǎn)符號(hào)后,其他不懂程序的人看了仍然覺得很亂,那么你很可能層沒分好”。比如前面的btn的click:
{
?字符串 用戶名是 下拉框 選擇值;
?字符串 密碼是 輸入框 值;
?如果 密碼是 空
???對(duì)話框(密碼空!);
?否則
?{
???用戶 這用戶;
???嘗試
??{
????這用戶 是 新的 用戶(用戶名);
??}
??捕捉(錯(cuò)誤)
??{
????對(duì)話框(錯(cuò)誤 消息);
????返回;
??}
??如果 這用戶檢查密碼(密碼)
??{
????這用戶 設(shè)置狀態(tài);
????響應(yīng) 重定位(“。。。。。”);
??}
??否則
??{
????對(duì)話框(密碼錯(cuò)誤)
??}
}
代碼最好能讓不懂的人也能看懂到底在干什么。
最后,oldjacky的Datagrid刪除的例子“刪除”顯然在D層,但是不允許卻可能在C或U,如果在U沒什么說的了,如果在C,那么這種“不允許”的一個(gè)比較合理的實(shí)現(xiàn)方法就是在C層里遇到這種情況throw一下。當(dāng)U層里catch到該throw的時(shí)候,禁止刪除操作,這樣當(dāng)2個(gè)層同時(shí)有原因引起禁止時(shí),可以從代碼一眼看出這種禁止的來源。類似于前面的2種彈出錯(cuò)誤。
這里講3層式,其實(shí)是針對(duì)“大江上面建橋”來的,對(duì)于1米寬的小溪,在實(shí)際中可能一點(diǎn)用都沒有。不過現(xiàn)在我不可能去拿個(gè)長江大橋作例子來講,所以這里還是用這條簡單的小溪,講講怎么建橋。之所以講這么多廢話,是為了防止部分人看完此文之后“小小一個(gè)東西,搞那么麻煩干什么。。”其實(shí)這里講的不是具體的這個(gè)例子,而是分層的思想,理解這點(diǎn)非常重要。
下面我就我們大家日常見最多的例子來講,就是“用戶登錄”的例子。這個(gè)例子很簡單,但是麻雀雖小五臟俱全。從數(shù)據(jù)訪問到業(yè)務(wù)規(guī)則到界面全有了。
本文分2個(gè)部分,如果只想研究面向?qū)ο蟮乃枷?#xff0c;對(duì)實(shí)現(xiàn)已經(jīng)熟悉,可以跳過第一部分。
第一部分
新建一個(gè)空白解決方案。
然后:
①“添加”-“新建項(xiàng)目”-“其他項(xiàng)目”-“企業(yè)級(jí)模版項(xiàng)目”-“C#生成塊”-“數(shù)據(jù)訪問”(數(shù)據(jù)層,下簡稱D層)
②“添加”-“新建項(xiàng)目”-“其他項(xiàng)目”-“企業(yè)級(jí)模版項(xiàng)目”-“C#生成塊”-“業(yè)務(wù)規(guī)則”(業(yè)務(wù)層,下簡稱C層)
③“添加”-“新建項(xiàng)目”-“其他項(xiàng)目”-“企業(yè)級(jí)模版項(xiàng)目”-“C#生成塊”-“Web用戶界面”(界面層,下簡稱U層)
④右鍵點(diǎn)“解決方案”-“項(xiàng)目依賴項(xiàng)”,設(shè)置U依賴于D、C,C依賴于D。
⑤對(duì)U添加引用D、C,對(duì)C添加引用D。
到此為止,一個(gè)三層的架子建立起來了。
我上面說的很具體很“傻瓜”,知道的人覺得我廢話,其實(shí)我這段時(shí)間很強(qiáng)烈的感覺到非常多的人其實(shí)對(duì)這個(gè)簡單的過程完全不了解。雖然不反對(duì)建2個(gè)“空項(xiàng)目”和1個(gè)“Asp net Web應(yīng)用程序項(xiàng)目”也可以作為3層的框架,而且相當(dāng)多的人認(rèn)為其實(shí)這些“企業(yè)級(jí)模板項(xiàng)目”其實(shí)就是個(gè)空項(xiàng)目,這是一個(gè)誤區(qū)。沒錯(cuò),企業(yè)級(jí)模板項(xiàng)目你從解決方案資源管理器里看它是個(gè)什么也沒有的,但是你可以用記事本打開項(xiàng)目文件,看見不同了吧??有些東西在背后,你是看不見的,不過系統(tǒng)已經(jīng)做好了。也就是說,如果你在C層里的某個(gè)類里“using System Data SqlClineit”,或者使用一個(gè)SqlConnection對(duì)象,編譯時(shí)候不會(huì)出錯(cuò),但是會(huì)在“任務(wù)列表”里生成一些“策略警告”,警告你在C層里不要放應(yīng)該放在D層的東西(雖然就程序來說沒錯(cuò),但是可讀性可維護(hù)性就打了折扣)而這種功能,空項(xiàng)目是無法給你的。
我們知道建橋需要磚塊,應(yīng)該是先準(zhǔn)備好磚再來建橋,不過為了講解上的順序性和連貫性,簡單性。我們先建橋,建的過程中需要磚塊再現(xiàn)做,這樣就不會(huì)多出來“橋不需要的東西”。注意在實(shí)際中,還是應(yīng)該先準(zhǔn)備磚塊。
U層其實(shí)就是橋,C層是磚塊,D層是原料(石頭、沙子)。這也解釋前面為什么U層要引用、依賴D層(而不是U對(duì)C,C對(duì)D的層次),因?yàn)闃虺诵枰u頭,其實(shí)也需要石頭沙子。
我們?cè)赨層建一個(gè)Login aspx(這里插入一句,我不喜歡去把系統(tǒng)自動(dòng)生成的WebForm1 aspx拿來改成login或index或直接刪除,我一般留著它當(dāng)測試代碼用,等到整個(gè)系統(tǒng)凍結(jié)再把它移除就可以了。)添加1個(gè)TextBox(id=txt),一個(gè)DropDownList(id=ddl),一個(gè)Button(id=btn)。其中DropDownList用來選擇用戶名,button是提交按鈕, TextBox用來輸入密碼。
現(xiàn)在我們必須要添加的代碼分為2部分:
1、Page_load時(shí)對(duì)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(“密碼錯(cuò)誤!”);??
????????}?
?????}?
}?
講解1:DataManager 是D層中的一個(gè)類,提供常見的數(shù)據(jù)操作。GetOneColunm(string Table,string Colunm)方法返回一個(gè)只有1列的DataTable,值為數(shù)據(jù)庫中表名為Table,的Colunm列。 public?class?DataManager?
{?
????public?DataManager()?
?????{?
?????}?
?????public?static?DataTable?GetOneColunm(string?Table,string?Colunm)?
?????{?
??????????//此處省略相關(guān)代碼。返回指定表指定列?
?????}?
}?
其實(shí)這個(gè)地方演示的是在U層直接繞過C層訪問D層的例子,因?yàn)樵摻Y(jié)構(gòu)邏輯上很簡單,而且獲取用戶名并不是現(xiàn)實(shí)社會(huì)中的業(yè)務(wù)邏輯的一部分(僅僅是界面需要,因?yàn)樵谶@里其實(shí)用成2個(gè)TextBox的話完全不需要這一步)
講解2:定義一個(gè)User類的實(shí)例。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;?
?????}?
}?
注意到用戶類構(gòu)造函數(shù)中用了個(gè)throw來拋出用戶不存在的異常,在下面catch的時(shí)候用MessageBox(e. Message);來彈出“用戶不存在”的錯(cuò)誤。這里其實(shí)也是為了演示一個(gè)層間傳遞信息的手段,異常也是一種手段,雖然在這里其實(shí)可以有其他方式比如返回值,引用參數(shù)之類的直接用一個(gè)方法來獲得用戶是否存在的信息,沒必要放在構(gòu)造里,我這么做只是為了演示傳遞過程,在后面的有討論這種用法在分層模式下某種特殊情況的應(yīng)用以解決一些問題。這個(gè)類里又用了DataManager類的一個(gè)靜態(tài)方法IsIn(string Table,string str)該方法其實(shí)其實(shí)是執(zhí)行 “select * from Table where str”
這個(gè)Sql語句并在返回空的時(shí)候方法返回false,否則返回true。一個(gè)很簡單的方法。這里演示了C層對(duì)D層的調(diào)用。
順便說一句,因?yàn)樵赩S.Net中,項(xiàng)目的名稱會(huì)默認(rèn)地成為項(xiàng)目中的namespace,可以通過把所有自動(dòng)生成的代碼中的namespace改為“解決方案名稱”來使3個(gè)層可以無縫地自由調(diào)用。或者在調(diào)用的地方using一下其他層的空間名,看個(gè)人喜歡了。比如上面的Login.aspx.cs里需要using2個(gè),而User.cs里要using一個(gè)。
講解3:這里的檢查用戶密碼同樣用到User類的一個(gè)方法CheckPsw()而這個(gè)方法 又用到了IsIn()這里就不多說了。
大家注意到我們?cè)赨層的頁面里用MessageBox()方法來彈出對(duì)話框,其實(shí)這個(gè)方法寫在PageBase.cs里,是U層的另外一個(gè)文件,繼承Page類,Login類又繼承它,這個(gè)方法其實(shí)是把Response.Write(“<script>alert(\“”+ msg+“\”)</script>”)封裝起來了。
到此為止,登錄結(jié)束,例子的實(shí)現(xiàn)也說完了。不過只講了“然”,沒有講“所以然”。下面開始講“所以然”。
第二部分
作為對(duì)比,我們使用一個(gè)不面向?qū)ο蟮?#xff0c;不分層的Asp式的Aspx相同登錄作為對(duì)比。具體的Asp代碼我就不寫了,反正登錄哪都有。先來看看他們2者發(fā)生的遭遇(這是不幸的,卻偏偏是經(jīng)常發(fā)生的):
1、? 項(xiàng)目經(jīng)理突然說“不用SqlServer了,換成Access”(正版費(fèi)用問題)。看看2邊分別發(fā)生什么:3層這邊(A),把DataManager類里的連接改改(在實(shí)際情況下,極可能其實(shí)是改它的基類,它本身不用改),Web.config中把字符串換掉就完了。Asp式那邊(B),同樣要改Web.config,同樣要改連接什么的,修改量在這個(gè)具體的“小溪”例子上幾乎相同,在“大橋”例子上B應(yīng)該會(huì)稍微多改點(diǎn),不過也不會(huì)多很多。但是!請(qǐng)注意一點(diǎn),我們?cè)谛薷拇a的時(shí)候,主要時(shí)間和精力不是花在“改”這個(gè)動(dòng)作上,而是花在“要改什么地方”上和“尋找需要改的地方”上。在“大橋”上,B需要花費(fèi)多的多的時(shí)間,對(duì)大部分文件進(jìn)行查找和替換。A則僅僅在數(shù)據(jù)層里,另外2個(gè)層不需要任何修改。從這個(gè)角度出發(fā)我們想到2點(diǎn)原則:
a)??? 數(shù)據(jù)層必須要能夠保證數(shù)據(jù)庫的變動(dòng)(任何結(jié)構(gòu)變動(dòng)、類型變動(dòng))對(duì)其余各層的不透明性。也就是數(shù)據(jù)庫怎么變,其他層絕對(duì)不應(yīng)該變哪怕1行代碼!(web.config是整個(gè)應(yīng)用程序的配置,雖然在物理上存在于U層的文件夾中,但個(gè)人更愿意認(rèn)為它是獨(dú)立的不屬于任何層的,所以這里不計(jì)它)
b)??? 數(shù)據(jù)層越小越好(如果沒有這點(diǎn)原則,我們把整個(gè)所有的東西都放在數(shù)據(jù)層,那當(dāng)然數(shù)據(jù)庫變動(dòng)對(duì)外面無影響――因?yàn)橥饷鎺缀鯖]東西――但是這顯然不可行)。而且因?yàn)榍懊嫖覀冋f了,大部分時(shí)間花在“找”上面,你小點(diǎn),找起來也容易點(diǎn)。
2、? 客戶突然提出B/S版的不好,要換成C/S版的。對(duì)于(B)來說,這是晴天霹靂!!他的所有工作都要重新做,(或者幾乎所有工作),雖然他有很多代碼還可以用,不過他在未來一小段時(shí)間就必須不斷在“復(fù)制-粘貼”中使用以前的代碼。(A)發(fā)生了什么??如果你細(xì)心看會(huì)發(fā)現(xiàn)(A)之需要新建個(gè)項(xiàng)目“Windows用戶界面”(和前面一樣,添加引用,項(xiàng)目依賴),拖幾個(gè)控件到上面,把控件名字起成txt,ddl,btn,然后把click代碼和Pageload代碼復(fù)制過去,(居然。。。)連1行代碼都不需要修改!!!!當(dāng)然,這是比較極端的例子(win和web都有TextBox,DropDownList,Button3種控件,而且我們?cè)赑ageBase里定義的方法MessageBox()又剛好和win里面方法同名。。。)不過盡管有這么多巧合我們?nèi)匀豢梢砸苍敢庀嘈?#xff0c;在“大橋”上,(A)將比(B)少做很多工作。從這個(gè)角度出發(fā)我們又想到2點(diǎn)類似原則:
a)??? 界面層應(yīng)該保證界面的任何變化都不需要修改其他層的內(nèi)容(不管這個(gè)具體的例子把ddl改為另外一個(gè)TextBox,或是把B/S改為C/S)
b)??? 界面層越小越好(理由同上。)
3、? 除開了界面層和數(shù)據(jù)層,(如果你的方案中只有3個(gè)層的話)剩下的就都是邏輯層的內(nèi)容了。所以和前面的相對(duì)應(yīng),我們可以得出結(jié)論:
a)??? 邏輯層應(yīng)當(dāng)不受數(shù)據(jù)庫和界面變動(dòng)的影響而需要修改。
b)??? 邏輯層越大越好(因?yàn)榱硗?層越小越好。。。)
有了最基本的原則,我們應(yīng)該來討論下,根據(jù)原則,要怎么分層的問題:
1、? PageBase.cs 應(yīng)該放在哪個(gè)層?根據(jù)上面的原則,應(yīng)該放在C層。但是實(shí)際上我習(xí)慣放在U層,或者放在另外一個(gè)(第4個(gè)層,通用底層,在比數(shù)據(jù)層還低的位置)層里。到底放在什么地方,我最開始的做法是在C層,因?yàn)榘瓷厦鏆w納的原則,就應(yīng)該放在C,但是后來一段時(shí)間我習(xí)慣于“四層式”之后就把它放在通用底層(下簡稱B層,該層同時(shí)也放如本來在D層中的SqlHelper類等,包括原來3層中所有“通用”的類,這里通用的意思是說其他系統(tǒng)也可以用的到而不需要修改,這個(gè)層通常不用解決方案名稱而用公司、小組名稱等作為namespace,在有新項(xiàng)目的時(shí)候在建解決方案的時(shí)候就可以“添加現(xiàn)有項(xiàng)目”,簡單的加進(jìn)去并不斷積累,實(shí)踐中對(duì)提高效率和代碼重用有比較大作用。)不過如果只有3層,我現(xiàn)在傾向于把PageBase放在U層。主要因?yàn)樽罱欢螡撔难芯棵嫦驅(qū)ο蟮姆治鲈O(shè)計(jì)的心得。說起來又是一大匹布沒完,不過我又在前面的“原則”上加1條:“如果某個(gè)類,僅為了某層的某種特殊實(shí)現(xiàn)而存在,那么它必須放在該層”,比如PageBase是為了U層的特殊實(shí)現(xiàn)(B/S實(shí)現(xiàn))而存在,又比如SqlHelper是為了D層的特殊實(shí)現(xiàn)(SqlServer數(shù)據(jù)庫)而存在。所以對(duì)應(yīng)的,它們必須分別放在U層和D層(如果不加這條的話按前面他們都該放在C層,因?yàn)镃層越大越好,而且數(shù)據(jù)庫和界面的變動(dòng)不需要改動(dòng)這2個(gè)類-雖然它們可能因改動(dòng)而沒有用了,不過還是不需要去修改它們)
2、? Oldjacky曾經(jīng)和我談到一個(gè)問題:Datagrid中允許作刪除操作,但是如果當(dāng)前僅余下最后一條記錄,則不允許這個(gè)刪除操作!那么該刪除應(yīng)該放在C層還是D層還是U層?我覺得應(yīng)該從另外一個(gè)角度來考慮:
a)??? 這種“不允許”是“業(yè)務(wù)規(guī)則的不允許”(比如表內(nèi)的數(shù)據(jù)表示當(dāng)前在店里的職員,刪除表示職員離開店里-可能去拿貨什么的,添加表示職員回來,當(dāng)柜臺(tái)只有一名職員時(shí),顯然他絕對(duì)不能離開去送貨),這個(gè)時(shí)候,此“禁止刪除”的操作應(yīng)該產(chǎn)生在C層。
b)??? 這種“不允許”是“程序?qū)崿F(xiàn)的不允許”(比如當(dāng)這里為空的時(shí)候會(huì)引起其他地方比如ToString()方法產(chǎn)生“未將對(duì)象的引用設(shè)置到對(duì)象的實(shí)例……”的錯(cuò)誤,或程序設(shè)計(jì)者或項(xiàng)目經(jīng)理的主觀愿望希望它“不允許”以此來減少工作量或簡化程序)。這個(gè)時(shí)候,此“禁止刪除”可以放在U層(比如上面說的ToString)或D層(比如違反數(shù)據(jù)庫約束)
3、? 細(xì)心的人可能會(huì)發(fā)現(xiàn),前面的登錄例子里,用戶一共可以獲得3種彈出錯(cuò)誤分別是“空密碼”“密碼錯(cuò)誤”“用戶不存在”,而其中前2個(gè)是在U層里做的,“用戶不存在”卻是在C層里做的(我是指這個(gè)字符串)還是開始說的建橋,我這里是用“小溪建橋”來講解“大江建橋”所以故意在這里轉(zhuǎn)了個(gè)沒用的圈,就像在計(jì)算小溪上這塊木板到底夠用多少年,其實(shí)對(duì)小溪沒什么意義,只是為了講解大橋需要而加上去的,畢竟大橋需要這種考慮。我這里假設(shè)“用戶不存在需要彈出提示”是一種業(yè)務(wù)邏輯上的需要,而“未輸入密碼需要提示”則不是業(yè)務(wù)規(guī)則需要(比如實(shí)際業(yè)務(wù)中可以允許空密碼,但是項(xiàng)目經(jīng)理不同意,說一定要密碼)在這個(gè)登錄例子中其實(shí)根本沒有什么問題,但是在大項(xiàng)目里,如果這個(gè)東西不是業(yè)務(wù)規(guī)則的需要,就不應(yīng)該放在業(yè)務(wù)層,如果是一種業(yè)務(wù)規(guī)則,就要放在業(yè)務(wù)層。有助于業(yè)務(wù)模型與現(xiàn)實(shí)實(shí)體的銜接,也有益于業(yè)務(wù)邏輯更好地表現(xiàn)現(xiàn)實(shí)實(shí)體的特征。
到此為止,我再次歸納出我們的最終的原則:
1、? 如果某個(gè)類,僅為了某層的某種特殊實(shí)現(xiàn)而存在,那么它必須放在該層。
2、? 數(shù)據(jù)層應(yīng)當(dāng)在保證數(shù)據(jù)庫變化對(duì)其他層不可見的前提下盡量小。
3、? 界面層應(yīng)當(dāng)在保證界面變化對(duì)業(yè)務(wù)邏輯層不影響的前提下盡量小。
4、? 如果某個(gè)類不是業(yè)務(wù)規(guī)則的需要,就不應(yīng)該放在業(yè)務(wù)層,反之亦然。
5、? 邏輯層應(yīng)當(dāng)在保證數(shù)據(jù)庫或界面變化不會(huì)造成自身影響的前提下盡量大。
以上5點(diǎn)如果發(fā)生沖突,在找平衡點(diǎn)的時(shí)候,前面的要高于后面的。比如1和3沖突的時(shí)候更傾向于使用規(guī)則1。
第二部分結(jié)束
有一點(diǎn)應(yīng)該是“編程代碼習(xí)慣”和“面向?qū)ο蟆钡姆懂?#xff0c;不過因?yàn)楹头謱佑行╆P(guān)系,所以也說一下。“如果你的代碼,自己把它翻譯成中文并加必要的標(biāo)點(diǎn)符號(hào)后,其他不懂程序的人看了仍然覺得很亂,那么你很可能層沒分好”。比如前面的btn的click:
{
?字符串 用戶名是 下拉框 選擇值;
?字符串 密碼是 輸入框 值;
?如果 密碼是 空
???對(duì)話框(密碼空!);
?否則
?{
???用戶 這用戶;
???嘗試
??{
????這用戶 是 新的 用戶(用戶名);
??}
??捕捉(錯(cuò)誤)
??{
????對(duì)話框(錯(cuò)誤 消息);
????返回;
??}
??如果 這用戶檢查密碼(密碼)
??{
????這用戶 設(shè)置狀態(tài);
????響應(yīng) 重定位(“。。。。。”);
??}
??否則
??{
????對(duì)話框(密碼錯(cuò)誤)
??}
}
代碼最好能讓不懂的人也能看懂到底在干什么。
最后,oldjacky的Datagrid刪除的例子“刪除”顯然在D層,但是不允許卻可能在C或U,如果在U沒什么說的了,如果在C,那么這種“不允許”的一個(gè)比較合理的實(shí)現(xiàn)方法就是在C層里遇到這種情況throw一下。當(dāng)U層里catch到該throw的時(shí)候,禁止刪除操作,這樣當(dāng)2個(gè)層同時(shí)有原因引起禁止時(shí),可以從代碼一眼看出這種禁止的來源。類似于前面的2種彈出錯(cuò)誤。
總結(jié)
以上是生活随笔為你收集整理的[转] 三层开发中的层次划分的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 我的小作品(烂笔头)
- 下一篇: 兩台SQL Server數據同步解決方案