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