国产在线超碰,高清美女视频一区,欧美国产综合一区二区http://www.aygfsteel.com/johnnylzb/category/44856.html生活總是有太多的無奈與失望,讓我們以在努力學(xué)習(xí)和工作中獲得的成就感和快樂來沖淡它們。zh-cnSun, 27 Jun 2010 19:18:16 GMTSun, 27 Jun 2010 19:18:16 GMT60領(lǐng)域驅(qū)動設(shè)計系列文章(3)——有選擇性的使用領(lǐng)域驅(qū)動設(shè)計http://www.aygfsteel.com/johnnylzb/archive/2010/06/26/324563.htmlJohnny.LiangJohnny.LiangSat, 26 Jun 2010 09:47:00 GMThttp://www.aygfsteel.com/johnnylzb/archive/2010/06/26/324563.htmlhttp://www.aygfsteel.com/johnnylzb/comments/324563.htmlhttp://www.aygfsteel.com/johnnylzb/archive/2010/06/26/324563.html#Feedback0http://www.aygfsteel.com/johnnylzb/comments/commentRss/324563.htmlhttp://www.aygfsteel.com/johnnylzb/services/trackbacks/324563.html

領(lǐng)域驅(qū)動設(shè)計系列文章(3)——有選擇性的使用領(lǐng)域驅(qū)動設(shè)計

 

本系列的第一篇博文拋磚引玉,大談領(lǐng)域驅(qū)動設(shè)計的優(yōu)勢,這里筆者還是希望以客觀的態(tài)度,談?wù)勵I(lǐng)域驅(qū)動設(shè)計的缺點(diǎn)及其不適合使用的場景,以讓讀者可以有選擇性的使用領(lǐng)域驅(qū)動設(shè)計。

 

       我們知道,沒有最好,只有最合適,設(shè)計也是一樣。因此,所謂設(shè)計,就是以你和你的團(tuán)隊的知識、經(jīng)驗和智慧,全面充分的考慮各種內(nèi)外因素后,在你們的設(shè)計方案中作出合理的選擇的過程。而這些影響你們選擇的因素主要有:

 

  • 技術(shù)框架的特征和約束(如果你的項目決定使用C語言進(jìn)行開發(fā),那么首先在設(shè)計方法上,就需要使用面向過程而非面向?qū)ο蟮脑O(shè)計方法)。

 

  • 時間的壓力和約束(你永遠(yuǎn)不可能告訴你的老板,給我10年時間,我和我的團(tuán)隊將為你設(shè)計出世界上最優(yōu)秀的軟件)。

 

  • 你的團(tuán)隊的能力、經(jīng)驗、性格、價值觀等因素的約束(你不能期望一個長期從事遺留系統(tǒng)維護(hù)項目或大部分成員是缺乏經(jīng)驗的高校畢業(yè)生的團(tuán)隊能很好的按照你的設(shè)計意圖去實(shí)現(xiàn)你的高度抽象的優(yōu)秀設(shè)計,同時你也別指望一幫家里經(jīng)濟(jì)條件不錯,本著過來熬時間的家伙會樂意與你一起刻苦鉆研、精益求精)。

 

  • 你的系統(tǒng)的特征(如果你想把一個足夠簡單而且在可以預(yù)計的將來都不存在很大規(guī)模的需求變更的系統(tǒng)設(shè)計得很復(fù)雜,很精妙,具有很好的擴(kuò)展性,但要為此付出巨大的時間、人力成本,這顯然是一種不理智的過度設(shè)計(Over design))。

 

  • 其他外在因素的約束(你的項目需要參與投標(biāo),你必須壓縮人力、時間等以讓你的項目成本成為巨大的競爭資本)。

 

當(dāng)然,上述的考慮因素站在比較高的角度,通常是項目經(jīng)理、架構(gòu)師需要考慮的問題,但這當(dāng)中你應(yīng)該會得到一些啟發(fā)。回到我們的主題,我們首先看看,領(lǐng)域驅(qū)動設(shè)計相對于傳統(tǒng)的面向過程式的設(shè)計,有什么缺點(diǎn):

 

  • 復(fù)雜化:面向過程思維之所以一直很受歡迎,是因為它很直觀,非常符合大部分人的思維習(xí)慣,大部分人拿到一個問題,通常都是會很直觀的想第一步做什么、第二步做什么,如果怎樣,應(yīng)該怎樣,否則怎樣……,可以說,任何水平的程序員,都能很好的使用面向過程的方法進(jìn)行設(shè)計和開發(fā)。同時,由于我們教育水平的落后和整體IT環(huán)境的制約,可以這樣說,真正掌握面向?qū)ο笏季S和設(shè)計方法的程序員的比例非常低(雖然絕大部分都使用面向?qū)ο蟮恼Z言和工具),而本身面向?qū)ο笏季S要求人有很好的抽象思維能力,因為你需要把一個復(fù)雜的系統(tǒng)一層層的抽象為簡單的部分,需要把現(xiàn)實(shí)世界的事物(有些是可見的,但有些是不可見)合理的抽象為計算機(jī)世界的不同元素。這些都不是一些很容易做的事情,要做得好,就更難。


  • 團(tuán)隊的抗拒:如果你的團(tuán)隊(很大可能)大部分人都習(xí)慣于用很直觀的面向過程的方式進(jìn)行設(shè)計和開發(fā),你需要推動你的團(tuán)隊轉(zhuǎn)換思維來采用面向?qū)ο蟮姆绞竭M(jìn)行領(lǐng)域驅(qū)動設(shè)計,通常會遭到多數(shù)人的抗拒。因為人都是有惰性的,他們習(xí)慣安于現(xiàn)狀,而改變是痛苦的,他們要付出額外的努力,需要進(jìn)行學(xué)習(xí),但以筆者的經(jīng)驗,相當(dāng)一部分程序員,特別是有一定工作年限的程序員,他們從事IT工作都只是為了獲得一份不錯的報酬,因此他們的學(xué)習(xí)動力非常有限,而且,他們都相當(dāng)自負(fù),被說服的難度比較大。



  • 管理、開發(fā)和維護(hù)的成本高:復(fù)雜度更高,意味著你需要花更多的時間進(jìn)行設(shè)計,同時需要花出額外的時間進(jìn)行必要的培訓(xùn),而且需要有更完善的文檔(設(shè)計文檔,API文檔,培訓(xùn)文檔等)。領(lǐng)域驅(qū)動設(shè)計的抽象程度比較高,因此必需有良好的文檔,否則,隨著項目的不斷迭代、升級和維護(hù),它很容易因為后來者的誤解而慢慢回歸面向過程的設(shè)計,甚至?xí)兊?#8220;四不象”,領(lǐng)域驅(qū)動設(shè)計的成本優(yōu)勢是隨著時間的推移慢慢體現(xiàn)的(見下圖),如果出現(xiàn)這種情況,所有前面付出的努力都會付諸東流。


系統(tǒng)的初始階段,領(lǐng)域驅(qū)動設(shè)計需要付出更大的成本,但隨著時間的推移,領(lǐng)域驅(qū)動設(shè)計的成本效益優(yōu)勢會逐步顯現(xiàn)

  • 性能比較低:使用純面向?qū)ο蟮姆绞竭M(jìn)行領(lǐng)域驅(qū)動設(shè)計的程序,其系統(tǒng)開銷通常要比面向過程設(shè)計的程序高,從而性能相對較低(關(guān)于具體的例子,后續(xù)的博文會提及)。

 

那么,假設(shè)我們在時間、團(tuán)隊能力及各種資源都允許的情況下,是否就可以麻木的全盤使用領(lǐng)域驅(qū)動設(shè)計呢?正如本博文的標(biāo)題一樣,答案是否定的,我們需要有選擇性的使用。讓我們來看看一些指導(dǎo)性原則:

 

  • 使用領(lǐng)域驅(qū)動設(shè)計,并不代表整個系統(tǒng)的方方面面都必須遵從領(lǐng)域驅(qū)動設(shè)計的原則,需要根據(jù)實(shí)際情況,讓適合的部分使用領(lǐng)域驅(qū)動設(shè)計,讓不適合的部分使用面向過程的設(shè)計。


  • 對于那些業(yè)務(wù)規(guī)則非常簡單,通常只有增、刪、改、查的簡單操作,而且也不大可能發(fā)生大規(guī)模需求變更的模塊,可以讓業(yè)務(wù)實(shí)體成為一個“貧血模型”,例如一些基礎(chǔ)數(shù)據(jù):系統(tǒng)參數(shù)、商品類型、國家、地址信息(注:對于這點(diǎn),本人持保留態(tài)度,因為這些業(yè)務(wù)雖然非常簡單,但既然選擇了領(lǐng)取驅(qū)動設(shè)計,即使把這些業(yè)務(wù)實(shí)體設(shè)計為“充血模型”,即把極其簡單的業(yè)務(wù)邏輯也封裝在業(yè)務(wù)實(shí)體中,也并不比使用“貧血模型”成本高,而它卻帶來了統(tǒng)一設(shè)計風(fēng)格的好處)。


  • 對于查詢操作,特別是復(fù)雜的查詢操作,出于性能的考慮,可以用結(jié)構(gòu)化查詢邏輯一次性完成,并把這些邏輯封裝在Repository(即技術(shù)上的DAO)中(這方面的具體例子,后面關(guān)于“查詢通道”和“領(lǐng)域?qū)ο髠}庫”的博文會更具體的闡述)。我們可以看到,對于一些業(yè)務(wù)視圖,以及報表模塊,很明顯不適合使用面向?qū)ο蟮姆绞皆O(shè)計,因為這些“視圖”和“報表”,本質(zhì)上就不是業(yè)務(wù)實(shí)體。


  • 同樣出于性能的考慮,在業(yè)務(wù)實(shí)體的實(shí)現(xiàn)邏輯中,某些操作不適合過度偏執(zhí)的使用面向?qū)ο蠓绞健@纾?#8220;訂單”的“新增訂單明細(xì)”(order.addOrderItem(orderItem))中,如果業(yè)務(wù)邏輯規(guī)定一張訂單中包含優(yōu)惠商品的明細(xì)數(shù)目不能超過20條,使用面向?qū)ο蟮姆绞剑托枰延唵沃械乃杏唵蚊骷?xì)全部加載,然后逐個明細(xì)判斷其對應(yīng)的商品是否優(yōu)惠商品,再統(tǒng)計出優(yōu)惠商品的數(shù)目,這樣很明顯是低效率和高開銷的,這里只需要使用Repository提供的一個統(tǒng)計方法,用一個結(jié)構(gòu)化查詢邏輯返回統(tǒng)計結(jié)果即可,而這就是非面向?qū)ο蟮姆绞健?/span>


本博文給有志于領(lǐng)域驅(qū)動設(shè)計的讀者潑了一下冷水,提出一些“反模式”(Bitter),是為了讓讀者冷靜一下,在領(lǐng)域驅(qū)動設(shè)計過程中作出更靈活和更合理的選擇。關(guān)于這方面的論述,筆者在這里淺嘗則止,限于水平、經(jīng)驗和表達(dá)能力,不敢胡亂賣弄,建議讀者可以參考閱讀Martin Fowler的《Patterns of Enterprise Application Architecture》一書的相關(guān)觀點(diǎn)。



Johnny.Liang 2010-06-26 17:47 發(fā)表評論
]]>
領(lǐng)域驅(qū)動設(shè)計系列文章(2)——淺析VO、DTO、DO、PO的概念、區(qū)別和用處http://www.aygfsteel.com/johnnylzb/archive/2010/05/27/321968.htmlJohnny.LiangJohnny.LiangWed, 26 May 2010 16:07:00 GMThttp://www.aygfsteel.com/johnnylzb/archive/2010/05/27/321968.htmlhttp://www.aygfsteel.com/johnnylzb/comments/321968.htmlhttp://www.aygfsteel.com/johnnylzb/archive/2010/05/27/321968.html#Feedback6http://www.aygfsteel.com/johnnylzb/comments/commentRss/321968.htmlhttp://www.aygfsteel.com/johnnylzb/services/trackbacks/321968.html

上一篇文章作為一個引子,說明了領(lǐng)域驅(qū)動設(shè)計的優(yōu)勢,從本篇文章開始,筆者將會結(jié)合自己的實(shí)際經(jīng)驗,談及領(lǐng)域驅(qū)動設(shè)計的應(yīng)用。本篇文章主要討論一下我們經(jīng)常會用到的一些對象:VODTODOPO

由于不同的項目和開發(fā)人員有不同的命名習(xí)慣,這里我首先對上述的概念進(jìn)行一個簡單描述,名字只是個標(biāo)識,我們重點(diǎn)關(guān)注其概念:

 

概念:

VOView Object):視圖對象,用于展示層,它的作用是把某個指定頁面(或組件)的所有數(shù)據(jù)封裝起來。

DTOData Transfer Object):數(shù)據(jù)傳輸對象,這個概念來源于J2EE的設(shè)計模式,原來的目的是為了EJB的分布式應(yīng)用提供粗粒度的數(shù)據(jù)實(shí)體,以減少分布式調(diào)用的次數(shù),從而提高分布式調(diào)用的性能和降低網(wǎng)絡(luò)負(fù)載,但在這里,我泛指用于展示層與服務(wù)層之間的數(shù)據(jù)傳輸對象。

DODomain Object):領(lǐng)域?qū)ο螅褪菑默F(xiàn)實(shí)世界中抽象出來的有形或無形的業(yè)務(wù)實(shí)體。

POPersistent Object):持久化對象,它跟持久層(通常是關(guān)系型數(shù)據(jù)庫)的數(shù)據(jù)結(jié)構(gòu)形成一一對應(yīng)的映射關(guān)系,如果持久層是關(guān)系型數(shù)據(jù)庫,那么,數(shù)據(jù)表中的每個字段(或若干個)就對應(yīng)PO的一個(或若干個)屬性。

 

模型:

       下面以一個時序圖建立簡單模型來描述上述對象在三層架構(gòu)應(yīng)用中的位置


 

      

l         用戶發(fā)出請求(可能是填寫表單),表單的數(shù)據(jù)在展示層被匹配為VO

l         展示層把VO轉(zhuǎn)換為服務(wù)層對應(yīng)方法所要求的DTO,傳送給服務(wù)層。

l         服務(wù)層首先根據(jù)DTO的數(shù)據(jù)構(gòu)造(或重建)一個DO,調(diào)用DO的業(yè)務(wù)方法完成具體業(yè)務(wù)。

l         服務(wù)層把DO轉(zhuǎn)換為持久層對應(yīng)的PO(可以使用ORM工具,也可以不用),調(diào)用持久層的持久化方法,把PO傳遞給它,完成持久化操作。

l         對于一個逆向操作,如讀取數(shù)據(jù),也是用類似的方式轉(zhuǎn)換和傳遞,略。

 

VODTO的區(qū)別

       大家可能會有個疑問(在筆者參與的項目中,很多程序員也有相同的疑惑):既然DTO是展示層與服務(wù)層之間傳遞數(shù)據(jù)的對象,為什么還需要一個VO呢?對!對于絕大部分的應(yīng)用場景來說,DTOVO的屬性值基本是一致的,而且他們通常都是POJO,因此沒必要多此一舉,但不要忘記這是實(shí)現(xiàn)層面的思維,對于設(shè)計層面來說,概念上還是應(yīng)該存在VODTO,因為兩者有著本質(zhì)的區(qū)別,DTO代表服務(wù)層需要接收的數(shù)據(jù)和返回的數(shù)據(jù),而VO代表展示層需要顯示的數(shù)據(jù)。

       用一個例子來說明可能會比較容易理解:例如服務(wù)層有一個getUser的方法返回一個系統(tǒng)用戶,其中有一個屬性是gender(性別),對于服務(wù)層來說,它只從語義上定義:1-男性,2-女性,0-未指定,而對于展示層來說,它可能需要用“帥哥”代表男性,用“美女”代表女性,用“秘密”代表未指定。說到這里,可能你還會反駁,在服務(wù)層直接就返回“帥哥美女”不就行了嗎?對于大部分應(yīng)用來說,這不是問題,但設(shè)想一下,如果需求允許客戶可以定制風(fēng)格,而不同風(fēng)格對于“性別”的表現(xiàn)方式不一樣,又或者這個服務(wù)同時供多個客戶端使用(不同門戶),而不同的客戶端對于表現(xiàn)層的要求有所不同,那么,問題就來了。再者,回到設(shè)計層面上分析,從職責(zé)單一原則來看,服務(wù)層只負(fù)責(zé)業(yè)務(wù),與具體的表現(xiàn)形式無關(guān),因此,它返回的DTO,不應(yīng)該出現(xiàn)與表現(xiàn)形式的耦合。

       理論歸理論,這到底還是分析設(shè)計層面的思維,是否在實(shí)現(xiàn)層面必須這樣做呢?一刀切的做法往往會得不償失,下面我馬上會分析應(yīng)用中如何做出正確的選擇。

 

VODTO的應(yīng)用

       上面只是用了一個簡單的例子來說明VODTO在概念上的區(qū)別,本節(jié)將會告訴你如何在應(yīng)用中做出正確的選擇。

       在以下才場景中,我們可以考慮把VODTO二合為一(注意:是實(shí)現(xiàn)層面):

l         當(dāng)需求非常清晰穩(wěn)定,而且客戶端很明確只有一個的時候,沒有必要把VODTO區(qū)分開來,這時候VO可以退隱,用一個DTO即可,為什么是VO退隱而不是DTO?回到設(shè)計層面,服務(wù)層的職責(zé)依然不應(yīng)該與展示層耦合,所以,對于前面的例子,你很容易理解,DTO對于“性別”來說,依然不能用“帥哥美女”,這個轉(zhuǎn)換應(yīng)該依賴于頁面的腳本(如JavaScript)或其他機(jī)制(JSTLELCSS

l         即使客戶端可以進(jìn)行定制,或者存在多個不同的客戶端,如果客戶端能夠用某種技術(shù)(腳本或其他機(jī)制)實(shí)現(xiàn)轉(zhuǎn)換,同樣可以讓VO退隱

 

以下場景需要優(yōu)先考慮VODTO并存:

l         上述場景的反面場景

l         因為某種技術(shù)原因,比如某個框架(如Flex)提供自動把POJO轉(zhuǎn)換為UI中某些Field時,可以考慮在實(shí)現(xiàn)層面定義出VO,這個權(quán)衡完全取決于使用框架的自動轉(zhuǎn)換能力帶來的開發(fā)和維護(hù)效率提升與設(shè)計多一個VO所多做的事情帶來的開發(fā)和維護(hù)效率的下降之間的比對。

l         如果頁面出現(xiàn)一個“大視圖”,而組成這個大視圖的所有數(shù)據(jù)需要調(diào)用多個服務(wù),返回多個DTO來組裝(當(dāng)然,這同樣可以通過服務(wù)層提供一次性返回一個大視圖的DTO來取代,但在服務(wù)層提供一個這樣的方法是否合適,需要在設(shè)計層面進(jìn)行權(quán)衡)。

 

DTODO的區(qū)別

       首先是概念上的區(qū)別,DTO是展示層和服務(wù)層之間的數(shù)據(jù)傳輸對象(可以認(rèn)為是兩者之間的協(xié)議),而DO是對現(xiàn)實(shí)世界各種業(yè)務(wù)角色的抽象,這就引出了兩者在數(shù)據(jù)上的區(qū)別,例如UserInfoUser(對于DTODO的命名規(guī)則,請參見筆者前面的一篇博文),對于一個getUser方法來說,本質(zhì)上它永遠(yuǎn)不應(yīng)該返回用戶的密碼,因此UserInfo至少比User少一個password的數(shù)據(jù)。而在領(lǐng)域驅(qū)動設(shè)計中,正如第一篇系列文章所說,DO不是簡單的POJO,它具有領(lǐng)域業(yè)務(wù)邏輯。

 

DTODO的應(yīng)用

       從上一節(jié)的例子中,細(xì)心的讀者可能會發(fā)現(xiàn)問題:既然getUser方法返回的UserInfo不應(yīng)該包含password,那么就不應(yīng)該存在password這個屬性定義,但如果同時有一個createUser的方法,傳入的UserInfo需要包含用戶的password,怎么辦?在設(shè)計層面,展示層向服務(wù)層傳遞的DTO與服務(wù)層返回給展示層的DTO在概念上是不同的,但在實(shí)現(xiàn)層面,我們通常很少會這樣做(定義兩個UserInfo,甚至更多),因為這樣做并不見得很明智,我們完全可以設(shè)計一個完全兼容的DTO,在服務(wù)層接收數(shù)據(jù)的時候,不該由展示層設(shè)置的屬性(如訂單的總價應(yīng)該由其單價、數(shù)量、折扣等決定),無論展示層是否設(shè)置,服務(wù)層都一概忽略,而在服務(wù)層返回數(shù)據(jù)時,不該返回的數(shù)據(jù)(如用戶密碼),就不設(shè)置對應(yīng)的屬性。

       對于DO來說,還有一點(diǎn)需要說明:為什么不在服務(wù)層中直接返回DO呢?這樣可以省去DTO的編碼和轉(zhuǎn)換工作,原因如下:

l         兩者在本質(zhì)上的區(qū)別可能導(dǎo)致彼此并不一一對應(yīng),一個DTO可能對應(yīng)多個DO,反之亦然,甚至兩者存在多對多的關(guān)系。

l         DO具有一些不應(yīng)該讓展示層知道的數(shù)據(jù)

l         DO具有業(yè)務(wù)方法,如果直接把DO傳遞給展示層,展示層的代碼就可以繞過服務(wù)層直接調(diào)用它不應(yīng)該訪問的操作,對于基于AOP攔截服務(wù)層來進(jìn)行訪問控制的機(jī)制來說,這問題尤為突出,而在展示層調(diào)用DO的業(yè)務(wù)方法也會因為事務(wù)的問題,讓事務(wù)難以控制。

l         對于某些ORM框架(如Hibernate)來說,通常會使用“延遲加載”技術(shù),如果直接把DO暴露給展示層,對于大部分情況,展示層不在事務(wù)范圍之內(nèi)(Open session in view在大部分情況下不是一種值得推崇的設(shè)計),如果其嘗試在Session關(guān)閉的情況下獲取一個未加載的關(guān)聯(lián)對象,會出現(xiàn)運(yùn)行時異常(對于Hibernate來說,就是LazyInitiliaztionException)。

l         從設(shè)計層面來說,展示層依賴于服務(wù)層,服務(wù)層依賴于領(lǐng)域?qū)樱绻?/span>DO暴露出去,就會導(dǎo)致展示層直接依賴于領(lǐng)域?qū)樱@雖然依然是單向依賴,但這種跨層依賴會導(dǎo)致不必要的耦合。

 

對于DTO來說,也有一點(diǎn)必須進(jìn)行說明,就是DTO應(yīng)該是一個“扁平的二維對象”,舉個例子來說明:如果User會關(guān)聯(lián)若干個其他實(shí)體(例如AddressAccountRegion等),那么getUser()返回的UserInfo,是否就需要把其關(guān)聯(lián)的對象的DTO都一并返回呢?如果這樣的話,必然導(dǎo)致數(shù)據(jù)傳輸量的大增,對于分布式應(yīng)用來說,由于涉及數(shù)據(jù)在網(wǎng)絡(luò)上的傳輸、序列化和反序列化,這種設(shè)計更不可接受。如果getUser除了要返回User的基本信息外,還需要返回一個AccountIdAccountNameRegionIdRegionName,那么,請把這些屬性定義到UserInfo中,把一個“立體”的對象樹“壓扁”成一個“扁平的二維對象”,筆者目前參與的項目是一個分布式系統(tǒng),該系統(tǒng)不管三七二十一,把一個對象的所有關(guān)聯(lián)對象都轉(zhuǎn)換為相同結(jié)構(gòu)的DTO對象樹并返回,導(dǎo)致性能非常的慢。

 

 

DOPO的區(qū)別

       DOPO在絕大部分情況下是一一對應(yīng)的,PO是只含有get/set方法的POJO,但某些場景還是能反映出兩者在概念上存在本質(zhì)的區(qū)別:

l         DO在某些場景下不需要進(jìn)行顯式的持久化,例如利用策略模式設(shè)計的商品折扣策略,會衍生出折扣策略的接口和不同折扣策略實(shí)現(xiàn)類,這些折扣策略實(shí)現(xiàn)類可以算是DO,但它們只駐留在靜態(tài)內(nèi)存,不需要持久化到持久層,因此,這類DO是不存在對應(yīng)的PO的。

l         同樣的道理,某些場景下,PO也沒有對應(yīng)的DO,例如老師Teacher和學(xué)生Student存在多對多的關(guān)系,在關(guān)系數(shù)據(jù)庫中,這種關(guān)系需要表現(xiàn)為一個中間表,也就對應(yīng)有一個TeacherAndStudentPOPO,但這個PO在業(yè)務(wù)領(lǐng)域沒有任何現(xiàn)實(shí)的意義,它完全不能與任何DO對應(yīng)上。這里要特別聲明,并不是所有多對多關(guān)系都沒有業(yè)務(wù)含義,這跟具體業(yè)務(wù)場景有關(guān),例如:兩個PO之間的關(guān)系會影響具體業(yè)務(wù),并且這種關(guān)系存在多種類型,那么這種多對多關(guān)系也應(yīng)該表現(xiàn)為一個DO,又如:“角色”與“資源”之間存在多對多關(guān)系,而這種關(guān)系很明顯會表現(xiàn)為一個DO——“權(quán)限”。

l         某些情況下,為了某種持久化策略或者性能的考慮,一個PO可能對應(yīng)多個DO,反之亦然。例如客戶Customer有其聯(lián)系信息Contacts,這里是兩個一對一關(guān)系的DO,但可能出于性能的考慮(極端情況,權(quán)作舉例),為了減少數(shù)據(jù)庫的連接查詢操作,把CustomerContacts兩個DO數(shù)據(jù)合并到一張數(shù)據(jù)表中。反過來,如果一本圖書Book,有一個屬性是封面cover,但該屬性是一副圖片的二進(jìn)制數(shù)據(jù),而某些查詢操作不希望把cover一并加載,從而減輕磁盤IO開銷,同時假設(shè)ORM框架不支持屬性級別的延遲加載,那么就需要考慮把cover獨(dú)立到一張數(shù)據(jù)表中去,這樣就形成一個DO對應(yīng)對個PO的情況。

l         PO的某些屬性值對于DO沒有任何意義,這些屬性值可能是為了解決某些持久化策略而存在的數(shù)據(jù),例如為了實(shí)現(xiàn)“樂觀鎖”,PO存在一個version的屬性,這個version對于DO來說是沒有任何業(yè)務(wù)意義的,它不應(yīng)該在DO中存在。同理,DO中也可能存在不需要持久化的屬性。

 

DOPO的應(yīng)用

       由于ORM框架的功能非常強(qiáng)大而大行其道,而且JavaEE也推出了JPA規(guī)范,現(xiàn)在的業(yè)務(wù)應(yīng)用開發(fā),基本上不需要區(qū)分DOPOPO完全可以通過JPAHibernate Annotations/hbm隱藏在DO之中。雖然如此,但有些問題我們還必須注意:

l         對于DO中不需要持久化的屬性,需要通過ORM顯式的聲明,如:在JPA中,可以利用@Transient聲明。

l         對于PO中為了某種持久化策略而存在的屬性,例如version,由于DOPO合并了,必須在DO中聲明,但由于這個屬性對DO是沒有任何業(yè)務(wù)意義的,需要讓該屬性對外隱藏起來,最常見的做法是把該屬性的get/set方法私有化,甚至不提供get/set方法,但對于Hibernate來說,這需要特別注意,由于Hibernate從數(shù)據(jù)庫讀取數(shù)據(jù)轉(zhuǎn)換為DO時,是利用反射機(jī)制先調(diào)用DO的空參數(shù)構(gòu)造函數(shù)構(gòu)造DO實(shí)例,然后再利用JavaBean的規(guī)范反射出set方法來為每個屬性設(shè)值,如果不顯式聲明set方法,或把set方法設(shè)置為private,都會導(dǎo)致Hibernate無法初始化DO,從而出現(xiàn)運(yùn)行時異常,可行的做法是把屬性的set方法設(shè)置為protected

l         對于一個DO對應(yīng)多個PO,或者一個PO對應(yīng)多個DO的場景,以及屬性級別的延遲加載,Hibernate都提供了很好的支持,請參考Hibnate的相關(guān)資料。

 

 

    到目前為止,相信大家都已經(jīng)比較清晰的了解VODTODOPO的概念、區(qū)別和實(shí)際應(yīng)用了。通過上面的詳細(xì)分析,我們還可以總結(jié)出一個原則:分析設(shè)計層面和實(shí)現(xiàn)層面完全是兩個獨(dú)立的層面,即使實(shí)現(xiàn)層面通過某種技術(shù)手段可以把兩個完全獨(dú)立的概念合二為一,在分析設(shè)計層面,我們?nèi)匀唬ㄖ辽僭陬^腦中)需要把概念上獨(dú)立的東西清晰的區(qū)分開來,這個原則對于做好分析設(shè)計非常重要(工具越先進(jìn),往往會讓我們越麻木)。第一篇系列博文拋磚引玉,大唱領(lǐng)域驅(qū)動設(shè)計的優(yōu)勢,但其實(shí)領(lǐng)域驅(qū)動設(shè)計在現(xiàn)實(shí)環(huán)境中還是有種種的限制,需要選擇性的使用,正如我在《田七的智慧》博文中提到,我們不能永遠(yuǎn)的理想化的去選擇所謂“最好的設(shè)計”,在必要的情況下,我們還是要敢于放棄,因為最合適的設(shè)計才是最好的設(shè)計。本來,系列中的第二篇博文應(yīng)該是討論領(lǐng)取驅(qū)動設(shè)計的限制和如何選擇性的使用,但請原諒我的疏忽,下一篇系列博文會把這個主題補(bǔ)上,敬請關(guān)注。

Johnny.Liang 2010-05-27 00:07 發(fā)表評論
]]>
領(lǐng)域驅(qū)動設(shè)計系列文章(1)——通過現(xiàn)實(shí)例子顯示領(lǐng)域驅(qū)動設(shè)計的威力http://www.aygfsteel.com/johnnylzb/archive/2010/05/15/321057.htmlJohnny.LiangJohnny.LiangSat, 15 May 2010 13:58:00 GMThttp://www.aygfsteel.com/johnnylzb/archive/2010/05/15/321057.htmlhttp://www.aygfsteel.com/johnnylzb/comments/321057.htmlhttp://www.aygfsteel.com/johnnylzb/archive/2010/05/15/321057.html#Feedback14http://www.aygfsteel.com/johnnylzb/comments/commentRss/321057.htmlhttp://www.aygfsteel.com/johnnylzb/services/trackbacks/321057.html

領(lǐng)域驅(qū)動設(shè)計系列文章(1)——通過現(xiàn)實(shí)例子顯示領(lǐng)域驅(qū)動設(shè)計的威力

 

       曾經(jīng)參與過系統(tǒng)維護(hù)或是在現(xiàn)有系統(tǒng)中進(jìn)行迭代開發(fā)的軟件工程師們,你們是否有過這樣的痛苦經(jīng)歷:當(dāng)需要修改一個Bug的時候,面對一個類中成百上千行的代碼,沒有注釋,千奇百怪的方法和變量名字,層層嵌套的方法調(diào)用,混亂不堪的結(jié)構(gòu),不要說準(zhǔn)確找到Bug所在的位置,就是要清晰知道一段代碼究竟是做了什么也非常困難,最終,改對了一個Bug,卻多冒出N個新Bug;同樣的情況,當(dāng)你拿到一份新的需求,需要在現(xiàn)有系統(tǒng)中添加功能的時候,面對一行行完全過程式的代碼,需要使用一個功能時,不知道是應(yīng)該自己編寫,還是應(yīng)該尋找是否已經(jīng)存在的方法,編寫一個非常簡單的新、刪、改功能,卻要費(fèi)盡九牛二虎之力,最終發(fā)現(xiàn),系統(tǒng)存在著太多的重復(fù)邏輯,閱讀、測試、修改非常困難。在經(jīng)歷了這些痛苦之后,你們是否會不約而同的發(fā)出一個感慨:與其進(jìn)行系統(tǒng)維護(hù)和迭代開發(fā),還不如重新設(shè)計開發(fā)一個新的系統(tǒng)來得痛快?

       面對這一系列讓軟件嵌入無底泥潭的問題,基于面向?qū)ο笏枷氲念I(lǐng)域驅(qū)動設(shè)計方法是一個很好的解決方法。從事過系統(tǒng)設(shè)計的富有經(jīng)驗的設(shè)計師們,對職責(zé)單一原則、信息專家、充血/貧血模型、模型驅(qū)動設(shè)計這些名詞或概念應(yīng)該不會感到陌生。面向?qū)ο蟮脑O(shè)計大師Martin Fowler不止一次的在他的Blog和著作《企業(yè)應(yīng)用架構(gòu)模式》中倡導(dǎo)過上述概論在設(shè)計中的巨大威力,而另外一位領(lǐng)域模型的出色專家Eric Evans的著作《領(lǐng)域驅(qū)動設(shè)計》也為我們提供了不少寶貴的經(jīng)驗和方法。

       筆者從事系統(tǒng)設(shè)計多年,將會在本系列文章中把本人對領(lǐng)域驅(qū)動設(shè)計的理解,結(jié)合工作過程中積累的實(shí)際項目經(jīng)驗進(jìn)行淺析,希望與大家交流學(xué)習(xí)。

       在本系列博文的開篇中,我將會拿出一個顯示的例子,先用傳統(tǒng)的面向過程方式,使用貧血模型進(jìn)行設(shè)計,然后再逐步加入需求變更,讓讀者發(fā)現(xiàn),隨著系統(tǒng)的不斷變更,基于貧血模型的設(shè)計將會讓系統(tǒng)慢慢陷入泥潭,越來越難于維護(hù),然后再用基于面向?qū)ο蟮念I(lǐng)域驅(qū)動設(shè)計重新上述過程,通過對比展示領(lǐng)域驅(qū)動設(shè)計對于復(fù)雜的業(yè)務(wù)系統(tǒng)的威力。


       假設(shè)現(xiàn)在有一個銀行支付系統(tǒng)項目,其中的一個重要的業(yè)務(wù)用例是賬戶轉(zhuǎn)賬業(yè)務(wù)。系統(tǒng)使用迭代的方式進(jìn)行開發(fā),在1.0版本中,該用例的功能需求非常簡單,事件流描述如下:

主事件流:

1)  用戶登錄銀行的在線支付系統(tǒng)

2)  選擇用戶在該銀行注冊的網(wǎng)上銀行賬戶

3)  選擇需要轉(zhuǎn)賬的目標(biāo)賬戶,輸入轉(zhuǎn)賬金額,申請轉(zhuǎn)賬

4)  銀行系統(tǒng)檢查轉(zhuǎn)出賬戶的金額是否足夠

5)  從轉(zhuǎn)出賬戶中扣除轉(zhuǎn)出金額(debit),更新轉(zhuǎn)出賬戶的余額

6)  把轉(zhuǎn)出金額加入到轉(zhuǎn)入賬戶中(credit),更新轉(zhuǎn)入賬戶的余額

備選事件流:

4a)如果轉(zhuǎn)出賬戶中的余額不足,轉(zhuǎn)賬失敗,返回錯誤信息

 

面向過程的設(shè)計方式(貧血模型)

 

設(shè)計方案如下(忽略展示層部分):

1)  設(shè)計一個賬戶交易服務(wù)接口AccountingService,設(shè)計一個服務(wù)方法transfer(),并提供一個具體實(shí)現(xiàn)類AccountingServiceImpl,所有賬戶交易業(yè)務(wù)的業(yè)務(wù)邏輯都置于該服務(wù)類中。

2)  提供一個AccountInfo和一個Account,前者是一個用于與展示層交換賬戶數(shù)據(jù)的賬戶數(shù)據(jù)傳輸對象,后者是一個賬戶實(shí)體(相當(dāng)于一個EntityBean),這兩個對象都是普通的JavaBean,具有相關(guān)屬性和簡單的get/set方法。

 

下面是AccountingServiceImpl.transfer()方法的實(shí)現(xiàn)邏輯(偽代碼):

 


public class AccountingServiceImpl implements AccountingService {

       
public void transfer(Long srcAccountId,Long destAccountId,BigDecimal amount) throws AccountingServiceException {

              Account srcAccount 
= accountRepository.getAccount(srcAccountId);

              Account destAccount 
= accountRepository.getAccount(destAccountId);

              
if(srcAccount.getBalance().compareTo(amount)<0)

                     
throw new AccountingServiceException(AccountingService.BALANCE_IS_NOT_ENOUGH);

              srcAccount.setBalance(srcAccount.getBalance().sbustract(amount));

              destAccount.setBalance(destAccount.getBalance().add(amount));

       }

}

 

public class Account implements DomainObject {

       
private Long id;

       
private Bigdecimal balance;

      

/**

 * getter/setter

 
*/

}

 

       可以看到,由于1.0版本的功能需求非常簡單,按面向過程的設(shè)計方式,把所有業(yè)務(wù)代碼置于AccountingServiceImpl中完全沒有問題。

       這時候,新需求來了,在1.0.1版本中,需要為賬戶轉(zhuǎn)賬業(yè)務(wù)增加如下功能,在轉(zhuǎn)賬時,首先需要判斷賬戶是否可用,然后,賬戶的余額還要分成兩部分:凍結(jié)部分和活躍部分,處于凍結(jié)部分的金額不能用于任何交易業(yè)務(wù),我們來看看變更后的代碼:

 


public class AccountingServiceImpl implements AccountingService {

       
public void transfer(Long srcAccountId,Long destAccountId,BigDecimal amount) throws AccountingServiceException {

              Account srcAccount 
= accountRepository.getAccount(srcAccountId);

              Account destAccount 
= accountRepository.getAccount(destAccountId);

              
if(!srcAccount.isActive() || !destAccount.isActive())

                     
throw new AccountingServiceException(AccountingService.ACCOUNT_IS_NOT_AVAILABLE);

              BigDecimal availableAmount 
= srcAccount.getBalance().substract(srcAccount.getFrozenAmount());

              
if(availableAmount.compareTo(amount)<0)

                     
throw new AccountingServiceException(AccountingService.BALANCE_IS_NOT_ENOUGH);

              srcAccount.setBalance(srcAccount.getBalance().sbustract(amount));

              destAccount.setBalance(destAccount.getBalance().add(amount));

       }

}

 

public class Account implements DomainObject {

       
private Long id;

       
private BigDecimal balance;

       
private BigDecimal frozenAmount;

      

/**

 * getter/setter

 
*/

}

 

       可以看到,情況變得稍微復(fù)雜了,這時候,1.0.2的需求又來了,需要在每次交易成功后,創(chuàng)建一個交易明細(xì)賬,于是,我們又必須在transfer()方面里面增加創(chuàng)建并持久化交易明細(xì)賬的業(yè)務(wù)邏輯:

             

     AccountTransactionDetails details= new AccountTransactionDetails(…);
     accountRepository.save(details);

      

       業(yè)務(wù)需求不斷復(fù)雜化:賬戶每筆轉(zhuǎn)賬的最大額度需要由其信用指數(shù)確定、需要根據(jù)銀行的手續(xù)費(fèi)策略計算并扣除一定的手續(xù)費(fèi)用……,隨著業(yè)務(wù)的復(fù)雜化,transfer()方法的邏輯變得越來越復(fù)雜,逐漸形成了上文所述的成百上千行代碼。有經(jīng)驗的程序員可能會做出類此“方法抽取”的重構(gòu),把轉(zhuǎn)賬業(yè)務(wù)按邏輯劃分成若干塊:判斷余額是否足夠、判斷賬戶的信用指數(shù)以確定每筆最大轉(zhuǎn)賬金額、根據(jù)銀行的手續(xù)費(fèi)策略計算手續(xù)費(fèi)、記錄交易明細(xì)賬……,從而使代碼更加結(jié)構(gòu)化。這是一個好的開始,但還是顯然不足。

       假設(shè)某一天,系統(tǒng)需求增加一個新的模塊,為系統(tǒng)增加一個網(wǎng)上商城,讓銀行用戶可以進(jìn)行在線購物,而在線購物也存在著很多與賬戶貸記借記業(yè)務(wù)相同或相似的業(yè)務(wù)邏輯:判斷余額是否足夠、對賬戶進(jìn)行借貸操作(credit/debit)以改變余額、收取手續(xù)費(fèi)用、產(chǎn)生交易明細(xì)賬……

       面對這種情況,有兩種解決辦法:

1)  AccountingServiceImpl中的相同邏輯拷貝到OnlineShoppingServiceImplementation

2)  OnlineShoppingServiceImpl調(diào)用AccountingServiceImpl的相同服務(wù)

顯然,第二種方法比第一種方法更好,結(jié)構(gòu)更清晰,維護(hù)更容易。但問題在于,這樣就會形成網(wǎng)上商城服務(wù)模塊與賬戶收支服務(wù)模塊的不必要的依賴關(guān)系,系統(tǒng)的耦合度高了,如果系統(tǒng)為了更靈活的伸縮性,讓每個大業(yè)務(wù)模塊獨(dú)立進(jìn)行部署,還需要因為兩者的依賴關(guān)系建立分布式調(diào)用,這無疑增加了設(shè)計、開發(fā)和運(yùn)維的成本。

有經(jīng)驗的設(shè)計人員可能會發(fā)現(xiàn)第三種解決辦法:把相同的業(yè)務(wù)邏輯抽取成一個新的服務(wù),作為公共服務(wù)同時供上述兩個業(yè)務(wù)模塊使用。這只是筆者將會馬上討論的方案——使用領(lǐng)域驅(qū)動設(shè)計。

 

 

 

面向過程的領(lǐng)域驅(qū)動設(shè)計方式(充血模型)

 

       為了節(jié)省篇幅,這里就直接以最復(fù)雜的業(yè)務(wù)需求來進(jìn)行設(shè)計。

領(lǐng)域驅(qū)動設(shè)計的一個重要的概念是領(lǐng)域模型,首先,我們根據(jù)業(yè)務(wù)領(lǐng)域抽象出以下核心業(yè)務(wù)對象模型:


 

Account:賬戶,是整個系統(tǒng)的最核心的業(yè)務(wù)對象,它包括以下屬性:對象標(biāo)識、賬戶號、是否有效標(biāo)識、余額、凍結(jié)金額、賬戶交易明細(xì)集合、賬戶信用等級。

AccountTransactionDetails:賬戶交易明細(xì),它從屬于賬戶,每個賬戶有多個交易明細(xì),它包括以下屬性:對象標(biāo)識、所屬賬戶、交易類型、交易發(fā)生金額、交易發(fā)生時間。

AccountCreditDegree:賬戶信用等級,它用于限制賬戶的每筆交易發(fā)生金額,包含以下屬性:對象標(biāo)識、對應(yīng)賬戶、信用指數(shù)。

BankTransactionFeeCalculator:銀行交易手續(xù)費(fèi)用計算器,它包含一個常量:每筆交易的手續(xù)費(fèi)上限。

 

我們知道,領(lǐng)域?qū)ο蟪司哂凶陨淼膶傩院蜖顟B(tài)之外,它的一個很重要的標(biāo)志是,它具有屬于自己職責(zé)范圍之內(nèi)的行為,這些行為封裝了其領(lǐng)域內(nèi)的領(lǐng)域業(yè)務(wù)邏輯。于是,我們進(jìn)行進(jìn)一步的建模,根據(jù)業(yè)務(wù)需求為領(lǐng)域?qū)ο笤O(shè)計業(yè)務(wù)方法:


 

根據(jù)職責(zé)單一的原則,我們把功能需求中描述的功能合理的分配到不同的領(lǐng)域?qū)ο笾校?/span>

Account

  • credit:向銀行賬戶存入金額,貸記
  • debit:從銀行賬戶劃出金額,借記
  • transferTo:把固定金額轉(zhuǎn)入指定賬戶
  • createTransactionDetails:創(chuàng)建交易明細(xì)賬
  • updateCreditIndex:更新賬戶的信用指數(shù)

(我們可以看到,后兩個業(yè)務(wù)方法被聲明為protected,具體原因見后述)

 

AccountCreditDegree

  • getMaxTransactionAmount:獲取所屬賬戶的每筆交易最大金額

 

BankTransactionFeeCalculator

  • calculateTransactionFee:根據(jù)交易信息計算該筆交易的手續(xù)費(fèi)

 

經(jīng)過這樣的設(shè)計,前例中所有放置在服務(wù)對象的業(yè)務(wù)邏輯被分別劃入不同的負(fù)責(zé)相關(guān)職責(zé)的領(lǐng)域?qū)ο螽?dāng)中,下面的時序圖描述了AccountingServiceImpl的轉(zhuǎn)賬業(yè)務(wù)的實(shí)現(xiàn)邏輯(為了簡化邏輯,我們忽略掉事物、持久化等邏輯):


 

再看看AccountingServiceImpl.transfer()的實(shí)現(xiàn)邏輯:

 


public class AccountingServiceImpl implements AccountingService {

       
public void transfer(Long srcAccountId,Long destAccountId,BigDecimal amount) throws AccountDomainException {

              Account srcAccount 
= accountRepository.getAccount(srcAccountId);

              Account destAccount 
= accountRepository.getAccount(destAccountId);

              srcAccount.transferTo(destAccount,amount);

       }

}

 

我們可以看到,上例那些復(fù)雜的業(yè)務(wù)邏輯:判斷余額是否足夠、判斷賬戶是否可用、改變賬戶余額、計算手續(xù)費(fèi)、判斷交易額度、產(chǎn)生交易明細(xì)賬……,都不再存在于AccountingServiceImplementationtransfer方法中,它們被委派給負(fù)責(zé)這些業(yè)務(wù)的領(lǐng)域?qū)ο蟮臉I(yè)務(wù)方法中去,現(xiàn)在應(yīng)該猜到為什么Account中有兩個方法被聲明為protected了吧,因為他們是在debitcredit方法被調(diào)用時,由這兩個方法調(diào)用的,對于AccountingServiceImpl來說,由于產(chǎn)生交易明細(xì)(createTransactionDetails)和更新賬戶信用指數(shù)(updateCreditIndex)都不屬于其職責(zé)范圍,它不需要也無權(quán)使用這些邏輯。

 

我們可以看到,使用領(lǐng)域驅(qū)動設(shè)計至少會帶來下述優(yōu)點(diǎn):

  • 業(yè)務(wù)邏輯被合理的分散到不同的領(lǐng)域?qū)ο笾校a結(jié)構(gòu)更加清晰,可讀性,可維護(hù)性更高。
  • 對象職責(zé)更加單一,內(nèi)聚度更高。
  • 復(fù)雜的業(yè)務(wù)模型可以通過領(lǐng)域建模(UML是一種主要方式)清晰的表達(dá),開發(fā)人員甚至可以在不讀源碼的情況下就能了解業(yè)務(wù)和系統(tǒng)結(jié)構(gòu),這有利于對現(xiàn)存的系統(tǒng)進(jìn)行維護(hù)和迭代開發(fā)。

 

再看看如果這時需要加入網(wǎng)上商城的一個新的模塊,開發(fā)人員需要怎么去做,還記得上面提過的第三種方案嗎?就是把賬戶貸記和借記的相關(guān)業(yè)務(wù)抽取到成一個公共服務(wù),同時供銀行在線支付系統(tǒng)和網(wǎng)上商城系統(tǒng)服務(wù),其實(shí)這個公共的服務(wù),本質(zhì)上就是這些具有領(lǐng)域邏輯的領(lǐng)域?qū)ο螅?/span>AccountAccountCreditDegree……,由此我們又可以發(fā)現(xiàn)領(lǐng)域驅(qū)動設(shè)計的一大優(yōu)點(diǎn):

  • 系統(tǒng)高度模塊化,代碼重用度高,不會出現(xiàn)太多的重復(fù)邏輯。

 

筆者經(jīng)驗尚淺,而且文筆拙劣,希望通過這樣的一個場景的分析比較,能讓讀者初步認(rèn)識到基于面向?qū)ο蟮念I(lǐng)域驅(qū)動設(shè)計的威力,并在實(shí)際項目中嘗試應(yīng)用。本篇是領(lǐng)取驅(qū)動設(shè)計系列博文的第一篇,在系列文章的第二篇博文中,筆者將會淺析VODTODOPO的概念、用處和區(qū)別,敬請各位對本系列博文感興趣的讀者關(guān)注并給予指導(dǎo)修正。

 

      



Johnny.Liang 2010-05-15 21:58 發(fā)表評論
]]>
“設(shè)計”你的代碼http://www.aygfsteel.com/johnnylzb/archive/2010/04/28/319551.htmlJohnny.LiangJohnny.LiangTue, 27 Apr 2010 16:51:00 GMThttp://www.aygfsteel.com/johnnylzb/archive/2010/04/28/319551.htmlhttp://www.aygfsteel.com/johnnylzb/comments/319551.htmlhttp://www.aygfsteel.com/johnnylzb/archive/2010/04/28/319551.html#Feedback8http://www.aygfsteel.com/johnnylzb/comments/commentRss/319551.htmlhttp://www.aygfsteel.com/johnnylzb/services/trackbacks/319551.html 我的回答是:“編碼本身就是一種設(shè)計,你可以設(shè)計你的代碼。”

其實(shí)正如概要設(shè)計與詳細(xì)設(shè)計,系統(tǒng)設(shè)計與架構(gòu)設(shè)計一樣,編碼與設(shè)計也是沒有明顯的邊界,每個正確成長的程序員,都必須從編碼開始,慢慢鍛煉抽象思維、邏輯思維、面向?qū)ο笏季S,然后慢慢的過渡到系統(tǒng)設(shè)計,再隨著經(jīng)驗和知識的積累,慢慢過渡到架構(gòu)設(shè)計。下面我將會以最近的一個手頭的編碼任務(wù),簡單介紹一下如何“設(shè)計”你的代碼。

任務(wù)是這樣的,某銀行支付系統(tǒng)的客戶端接收銀行用戶錄入的轉(zhuǎn)賬數(shù)據(jù),當(dāng)轉(zhuǎn)賬數(shù)據(jù)被審批通過后,狀態(tài)轉(zhuǎn)變?yōu)?#8220;transfer”,同時,該客戶端需要通過JMS以異步的方式向支付系統(tǒng)后臺發(fā)送一條帶有轉(zhuǎn)賬記錄(Instruction)的消息,后端在接收到信息之后,需要根據(jù)Instruction的一些相關(guān)信息,首先確定這筆轉(zhuǎn)賬數(shù)據(jù)是直接發(fā)送給真正進(jìn)行轉(zhuǎn)賬的清算(Clearing)銀行系統(tǒng),還是停留在后端系統(tǒng),等待后端系統(tǒng)中需要執(zhí)行的工作流程(work flow)。而后端系統(tǒng)需要對Instruction執(zhí)行的工作流程有兩個,同時需要根據(jù)Instruction的一些相關(guān)信息進(jìn)行選擇。
為了簡化復(fù)雜度,我這里假設(shè)系統(tǒng)有一個InstructionHandleMsgDrivenBean,該bean有一個onMessage()方法,所有業(yè)務(wù)邏輯需要在該方法中實(shí)現(xiàn)。

同時解釋一下詳細(xì)的業(yè)務(wù)細(xì)節(jié):
  • 判斷Instruction是否需要停留在后端等待執(zhí)行指定的工作流程有三個條件:xx、yy、zz,當(dāng)三個條件都為true時,停留。
  • 判斷Instruction需要走A流程還是B流程,由4個因素的組合確定,如果用“Y”代表true,“N”代表false,那么由這個四個因素組成的“XXXX”一共有16種組合,不同的組合分別走A和B流程,如:YYNN、YYNY to A,NNYY、NNNY to B,……不累贅。
好了,對于一個純編程人員來說,拿到這樣的需求,感覺邏輯很簡單,可以直接編碼了,于是,他開始一行一行的編寫代碼(偽代碼):

public void onMessage(InstructionInfo instructionInfo) {
    if(xx && yy && zz) { // 停留在后端等待執(zhí)行指定的工作流程
        // 根據(jù)每種組合進(jìn)行條件判斷,走哪個流程
        if(a==true && b==true && c==true && d==true {
            ...
        }
        else if(...) {...}
        else if(...) {...}
        ...
        else(...) {...}    
    }
}

這種做法是最為開發(fā)人員歡迎的,因為它簡單、直接,但這種做法也恰恰反映了開發(fā)人員的通病——使用Java編寫純面向過程的代碼。

好了,說了一大堆,如何“設(shè)計”你的代碼呢?答案是:使用面向?qū)ο笏季S:

我們拿到需求之后,可以分析,這個需求大體上分為兩部分:
  • 判斷是否需要停留在后端等待執(zhí)行指定的工作流程的部分
  • 選擇走哪個工作流程的部分

有了這個前提,我可以設(shè)計出兩個職責(zé)單一的對象了:

public class InstructionHandleDecisionMaker {
    public static boolean isHandledByBackEnd(InstructionInfo info) {
        return (isXX(...) && isYY(...) && isZZ(...));
    }

    private booolean isXX(...) {
        //TODO Implement the logic
        return false;
    }
    private booolean isYY(...) {
        //TODO Implement the logic
        return false;
    }
    private booolean isZZ(...) {
        //TODO Implement the logic
        return false;
    }
}

public class InstructionWorkFlowSelector {
    private static Map mapping = new HashMap();
    static {
        mapping.input("YYNN",WorkFlow.A);
        mapping.input("NNYY",WorkFlow.B);
        ...
    }

    public static WorkFlow getWorkFlow(Instruction info) {
        StringBuilder result = new StringBuilder();
        result.append(isA(...)).append(isB(...));
        result.append(isC(...)).append(isD(...));
        return mapping.get(result.toString());
    }
    private static String isA(...) {
        //TODO Implment the logic
        return "N";
    }
    private static String isB(...) {
        //TODO Implment the logic
        return "N";
    }
    private static String isC(...) {
        //TODO Implment the logic
        return "N";
    }
    private static String isD(...) {
        //TODO Implment the logic
        return "N";
    }
}

可以看到,我先按職責(zé)劃分了類,再按職責(zé)抽取了私有方法,“框架”設(shè)計好 ,為了讓編譯通過,我上面完整的填寫了代碼的,然后加上TODO標(biāo)識,然后,我可以編寫我的onMessage方法了:

public void onMessage(InstructionInfo instructionInfo) {
    if( InstructionHandleDecisionMaker.isHandledByBackEnd(...) ) {
        WorkFlow wf =InstructionWorkFlowSelector.getWorkFlow(...);
        //TODO Implment the logic
    }
}

到目前為止,我已經(jīng)用純面向?qū)ο蟮乃季S方式“設(shè)計”好我的代碼了,這時,我思維非常清晰,因而代碼結(jié)構(gòu)也非常清晰,職責(zé)單一,內(nèi)聚高,耦合低,最后,我可以根據(jù)需求文檔的細(xì)節(jié)(沒有描述)慢慢的編寫我的實(shí)現(xiàn)了。

復(fù)雜的事物總是由一些較簡單的事物組成,而這些較簡單的事物也是由更簡單的事物組成,如此類推。因此,在編寫代碼的時候,先用面向?qū)ο蟮乃季S把復(fù)雜的問題分解,再進(jìn)一步分解,最后把簡單的問題各個擊破,這就是一種設(shè)計。開發(fā)人員只要養(yǎng)成這種習(xí)慣,即使你每天都只是做最底層的編碼工作,其實(shí)你已經(jīng)在參與設(shè)計工作了,隨著知識和經(jīng)驗的累積,慢慢的,你從設(shè)計代碼開始,上升為設(shè)計類、方法,進(jìn)而是設(shè)計模塊,進(jìn)而設(shè)計子系統(tǒng),進(jìn)而設(shè)計系統(tǒng)……,最終,一步一步成為一個優(yōu)秀的架構(gòu)師。

最后,有一個真理奉獻(xiàn)給浮躁的程序員:

優(yōu)秀的架構(gòu)師、設(shè)計師,必定是優(yōu)秀的程序員,不要因為你的職位上升了,就放棄編碼。

補(bǔ)充說明:本博文純粹是討論一種思維習(xí)慣,不要把其做法生搬硬套,不管實(shí)際情況,直接在編碼的時候這樣做,不見得是最好的選擇。在實(shí)際編碼中,有如下問題你必須考慮:
  • 你需要考慮業(yè)務(wù)邏輯的可重用性和復(fù)雜程度,是否有必要設(shè)計出新的類或抽取新的私有方法來封裝邏輯,或者直接在原方法上編碼(如果足夠簡單)。
  • 新的業(yè)務(wù)邏輯,是否在某些地方已經(jīng)存在,可以復(fù)用,即使不存在,這些邏輯是應(yīng)該封裝到新的類中,還是應(yīng)該放置到現(xiàn)有的類中,這需要進(jìn)行清晰的職責(zé)劃分。
  • 需要在設(shè)計和性能上作出權(quán)衡。
  • 如果在現(xiàn)成的系統(tǒng)中增加新的功能,而現(xiàn)成系統(tǒng)的編碼風(fēng)格與你想要的相差很遠(yuǎn),但你又沒有足夠的時間成本來進(jìn)行重構(gòu),那么還是應(yīng)該讓你的代碼與現(xiàn)成系統(tǒng)保持一致的風(fēng)格。



Johnny.Liang 2010-04-28 00:51 發(fā)表評論
]]>
不進(jìn)行業(yè)務(wù)層校驗的典型例子——某國內(nèi)著名第三方支付平臺的致命漏洞http://www.aygfsteel.com/johnnylzb/archive/2010/04/27/319531.htmlJohnny.LiangJohnny.LiangTue, 27 Apr 2010 13:51:00 GMThttp://www.aygfsteel.com/johnnylzb/archive/2010/04/27/319531.htmlhttp://www.aygfsteel.com/johnnylzb/comments/319531.htmlhttp://www.aygfsteel.com/johnnylzb/archive/2010/04/27/319531.html#Feedback0http://www.aygfsteel.com/johnnylzb/comments/commentRss/319531.htmlhttp://www.aygfsteel.com/johnnylzb/services/trackbacks/319531.html筆者在N年前曾經(jīng)參與一個第三方支付平臺的設(shè)計開發(fā)工作,這過程中研究了某個國內(nèi)著名的第三方支付平臺,就是因為該平臺的設(shè)計者輕易

的相信外部傳入的數(shù)據(jù),不進(jìn)行校驗,導(dǎo)致被我發(fā)現(xiàn)了一個致命的漏洞,從而測試性的盜取了2分錢,這個事情一直到2個月后,該平臺通過

與網(wǎng)銀對賬才發(fā)現(xiàn),由于不知道這個致命的漏洞是否已經(jīng)修復(fù),避免有惡意的人使用該方式去做違法的事情,我在這里只簡單描述該漏洞,

而不公開該支付平臺的真實(shí)身份。

該漏洞出現(xiàn)在充值業(yè)務(wù)上,實(shí)現(xiàn)邏輯請看下面的Sequence diagram:




描述一下流程:

1.客戶打開第三方支付平臺的充值頁面,選擇一個銀行,填入充值數(shù)據(jù)(充值虛擬賬戶、充值金額等),提交表單。
2.充值頁面把數(shù)據(jù)(包括充值數(shù)據(jù),第三方支付平臺在該銀行注冊的商戶標(biāo)識)使用銀行提供的加密工具進(jìn)行加密,傳遞給指定銀行的網(wǎng)銀系統(tǒng)前臺。
3.網(wǎng)銀系統(tǒng)前臺產(chǎn)生一張轉(zhuǎn)賬訂單,并要求用戶輸入賬號、密碼,然后提交。
4.網(wǎng)銀系統(tǒng)后端校驗賬號和密碼,然后根據(jù)用戶賬號,充值金額,第三方支付平臺商戶標(biāo)識,把金額從用戶賬號轉(zhuǎn)賬到第三方支付平臺賬號。
5.網(wǎng)銀系統(tǒng)把確認(rèn)數(shù)據(jù)(充值虛擬賬戶、充值金額、充值結(jié)果、轉(zhuǎn)入商戶標(biāo)識等)加密后重定向到第三方支付平臺結(jié)果頁面。
6.頁面把確認(rèn)數(shù)據(jù)提交給后臺,并向客戶顯示充值成功。
7.第三方支付平臺后臺接收到確認(rèn)數(shù)據(jù),把虛擬金額充入虛擬賬戶,完成整個流程。

漏洞描述:

我們可以看到,用戶在第三方支付平臺充值頁面提交數(shù)據(jù),和網(wǎng)銀向第三方支付平臺發(fā)送確認(rèn)數(shù)據(jù),都需要對數(shù)據(jù)進(jìn)行非對稱加密,但在當(dāng)

時,我發(fā)現(xiàn)某一銀行,竟然不提供發(fā)送充值數(shù)據(jù)時的加密工具,這意味著,選擇這家銀行,用戶必須以明文的格式提交充值數(shù)據(jù),好了,這

下我可以嘗試進(jìn)行man in the middle attack了,在我提交表單一刻,我把頁面的HTML腳本修改了,把提交時由第三方支付平臺指定的在所

選銀行注冊的商戶標(biāo)識修改為另外一個商戶的標(biāo)識(假設(shè)是我自己注冊的商戶),OK,這意味著我的充值金額從我的賬號轉(zhuǎn)到我修改后的注

冊商戶賬號,這時,該網(wǎng)銀把確認(rèn)數(shù)據(jù)(注意,包括轉(zhuǎn)入商戶標(biāo)識,這時的商戶標(biāo)識是我修改后的標(biāo)識)發(fā)送回第三方支付平臺,其實(shí),如

果第三方支付平臺如果能進(jìn)一步校驗確認(rèn)數(shù)據(jù)中的注冊商戶標(biāo)識,就能發(fā)現(xiàn)注冊商戶標(biāo)識被惡意篡改,從而導(dǎo)致充值失敗,但該平臺的設(shè)計

人員卻假設(shè)之前的充值頁面已經(jīng)自動填入了自己的注冊商戶標(biāo)識,這里并不作校驗,從而導(dǎo)致這個致命的漏洞,錢沒有轉(zhuǎn)到自己的賬號上,

而自己卻把等額的虛擬資金充值到了第三方虛擬賬戶中。


特別聲明:本文內(nèi)容純屬用于技術(shù)研討,任何人嘗試使用本文的信息實(shí)施非法行為而導(dǎo)致的任何損失和責(zé)任,本人概不負(fù)責(zé)。

Johnny.Liang 2010-04-27 21:51 發(fā)表評論
]]>
一個非常簡單的例子,反映了很多開發(fā)人員的通病http://www.aygfsteel.com/johnnylzb/archive/2010/04/27/319527.htmlJohnny.LiangJohnny.LiangTue, 27 Apr 2010 13:43:00 GMThttp://www.aygfsteel.com/johnnylzb/archive/2010/04/27/319527.htmlhttp://www.aygfsteel.com/johnnylzb/comments/319527.htmlhttp://www.aygfsteel.com/johnnylzb/archive/2010/04/27/319527.html#Feedback8http://www.aygfsteel.com/johnnylzb/comments/commentRss/319527.htmlhttp://www.aygfsteel.com/johnnylzb/services/trackbacks/319527.html最近在項目中接到一個任務(wù),我負(fù)責(zé)后臺開發(fā),另一開發(fā)人員負(fù)責(zé)前臺開發(fā)。任務(wù)非常簡單,請看下面的類圖。



A和B有一個單向的關(guān)聯(lián)關(guān)系,現(xiàn)在要為A增加一個屬性boolean resident,該屬性值有如下簡單的業(yè)務(wù)邏輯決定(偽代碼):

if(a.x == a.b.x)
   resident = true;
else
   resident = false;

前臺的查看頁面需要顯示這個值,當(dāng)用戶修改a.x或者a.b.x時,要觸發(fā)一個事件,實(shí)時的在前臺更新resident,后臺需要以這個resident來

做某些業(yè)務(wù)邏輯的判斷。

我們知道,由于resident完全可以由a.x與a.b.x決定,屬于一個冗余數(shù)據(jù),不需要顯式的增加該屬性。很快,前臺的那位開發(fā)人員就輕松的

告訴我,這個任務(wù)已經(jīng)寫完了,原來他是在頁面寫了一個JS腳本,當(dāng)a.x或a.b.x發(fā)生改變時,觸發(fā)一個onChange()事件,里面就實(shí)現(xiàn)了上述

的邏輯。我一看,感覺問題來了,然后我告訴他,我已經(jīng)在后臺的服務(wù)提供了一個方法,用于判斷resident的值,他應(yīng)該在事件觸發(fā)的時候

,通過異步方式調(diào)用后臺方法來獲取reisdent的值。他一聽覺得非常奇怪,這么簡單的邏輯,為什么需要使用異步方式去調(diào)用后臺這么麻煩

,于是我就從設(shè)計的角度跟他解釋,resident由 a.x與a.b.x決定,這是一個業(yè)務(wù)邏輯,從職責(zé)劃分的設(shè)計原則分析,前臺只負(fù)責(zé)顯示邏輯,

業(yè)務(wù)邏輯屬于后臺的職責(zé)。接著,他又提出了疑問,說這里其實(shí)只有一行代碼,沒有必要分得那么清,這樣調(diào)用不但麻煩,而且性能相對較

低。

這個例子,反映出了很多開發(fā)人員的通病,沒有真正從事物的本質(zhì)考慮問題,只從代碼的數(shù)量去考慮。

上述的做法存在兩個問題:

1.開發(fā)人員沒有從職責(zé)劃分的角度考慮問題,而只是貪圖一時的方便,或者覺得簡單的一兩行代碼不需要進(jìn)行設(shè)計,但實(shí)際上,上面的問題

必然導(dǎo)致重復(fù)代碼。上面的例子,即使只有一行代碼,但在系統(tǒng)的前后臺卻出現(xiàn)了兩份相同的邏輯,假設(shè)以后某天業(yè)務(wù)邏輯發(fā)生了變化,那

么,必須在兩個地方(甚至更多的地方)進(jìn)行修改、測試,如果漏掉了一個地方,就會導(dǎo)致Bug的出現(xiàn)。作為一個開發(fā)人員,如果不注意這些

細(xì)節(jié),隨意的拷貝和重復(fù)代碼,長期下來,一個系統(tǒng)就會遍布無數(shù)的重復(fù)代碼,系統(tǒng)越往后期就越難維護(hù),最終陷于崩潰。

2.如果作為后臺開發(fā)人員,看到前臺已經(jīng)實(shí)現(xiàn)了這個邏輯,而在前臺往后臺傳遞數(shù)據(jù)的時候,把resident也傳遞進(jìn)來,從而認(rèn)為后臺就不需

要重復(fù)這段邏輯,直接拿著前臺傳過來的這個resident來進(jìn)行業(yè)務(wù)判斷,那么就可能會給系統(tǒng)帶來致命的漏洞,因為數(shù)據(jù)在傳輸過程中,很

可能被有意或無意的修改,不在后臺進(jìn)行業(yè)務(wù)規(guī)則校驗這個錯誤是常見的,但也是致命的。


有一個原則開發(fā)人員特別容易犯,也一定要切記:

在Web應(yīng)用中,相信客戶端提交的數(shù)據(jù)是正確的,不在業(yè)務(wù)層進(jìn)行校驗,這是致命的錯誤。

筆者會在下一篇博文中,詳細(xì)說明某國內(nèi)著名的第三方支付平臺,因為犯了這個低級的錯誤,而導(dǎo)致系統(tǒng)出現(xiàn)致命的漏洞,敬請關(guān)注。

Johnny.Liang 2010-04-27 21:43 發(fā)表評論
]]>
主站蜘蛛池模板: 固安县| 宝清县| 张家口市| 东阿县| 三原县| 遂平县| 页游| 洛川县| 驻马店市| 土默特右旗| 杭锦旗| 女性| 高陵县| 鄂尔多斯市| 郯城县| 英吉沙县| 常山县| 铜山县| 太湖县| 绥滨县| 大田县| 宣城市| 荥阳市| 安乡县| 龙口市| 金山区| 娱乐| 定结县| 南昌县| 巴楚县| 鹤庆县| 尤溪县| 顺义区| 名山县| 栖霞市| 荔波县| 东乌珠穆沁旗| 怀柔区| 津市市| 丘北县| 中牟县|