領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)系列文章(3)——有選擇性的使用領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)
本系列的第一篇博文拋磚引玉,大談?lì)I(lǐng)域驅(qū)動(dòng)設(shè)計(jì)的優(yōu)勢(shì),這里筆者還是希望以客觀的態(tài)度,談?wù)勵(lì)I(lǐng)域驅(qū)動(dòng)設(shè)計(jì)的缺點(diǎn)及其不適合使用的場(chǎng)景,以讓讀者可以有選擇性的使用領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)。
我們知道,沒(méi)有最好,只有最合適,設(shè)計(jì)也是一樣。因此,所謂設(shè)計(jì),就是以你和你的團(tuán)隊(duì)的知識(shí)、經(jīng)驗(yàn)和智慧,全面充分的考慮各種內(nèi)外因素后,在你們的設(shè)計(jì)方案中作出合理的選擇的過(guò)程。而這些影響你們選擇的因素主要有:
當(dāng)然,上述的考慮因素站在比較高的角度,通常是項(xiàng)目經(jīng)理、架構(gòu)師需要考慮的問(wèn)題,但這當(dāng)中你應(yīng)該會(huì)得到一些啟發(fā)。回到我們的主題,我們首先看看,領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)相對(duì)于傳統(tǒng)的面向過(guò)程式的設(shè)計(jì),有什么缺點(diǎn):
系統(tǒng)的初始階段,領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)需要付出更大的成本,但隨著時(shí)間的推移,領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)的成本效益優(yōu)勢(shì)會(huì)逐步顯現(xiàn)
那么,假設(shè)我們?cè)跁r(shí)間、團(tuán)隊(duì)能力及各種資源都允許的情況下,是否就可以麻木的全盤使用領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)呢?正如本博文的標(biāo)題一樣,答案是否定的,我們需要有選擇性的使用。讓我們來(lái)看看一些指導(dǎo)性原則:
本博文給有志于領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)的讀者潑了一下冷水,提出一些“反模式”(Bitter),是為了讓讀者冷靜一下,在領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)過(guò)程中作出更靈活和更合理的選擇。關(guān)于這方面的論述,筆者在這里淺嘗則止,限于水平、經(jīng)驗(yàn)和表達(dá)能力,不敢胡亂賣弄,建議讀者可以參考閱讀Martin Fowler的《Patterns of Enterprise Application Architecture》一書的相關(guān)觀點(diǎn)。
上一篇文章作為一個(gè)引子,說(shuō)明了領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)的優(yōu)勢(shì),從本篇文章開始,筆者將會(huì)結(jié)合自己的實(shí)際經(jīng)驗(yàn),談及領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)的應(yīng)用。本篇文章主要討論一下我們經(jīng)常會(huì)用到的一些對(duì)象:VO、DTO、DO和PO。
由于不同的項(xiàng)目和開發(fā)人員有不同的命名習(xí)慣,這里我首先對(duì)上述的概念進(jìn)行一個(gè)簡(jiǎn)單描述,名字只是個(gè)標(biāo)識(shí),我們重點(diǎn)關(guān)注其概念:
概念:
VO(View Object):視圖對(duì)象,用于展示層,它的作用是把某個(gè)指定頁(yè)面(或組件)的所有數(shù)據(jù)封裝起來(lái)。
DTO(Data Transfer Object):數(shù)據(jù)傳輸對(duì)象,這個(gè)概念來(lái)源于J2EE的設(shè)計(jì)模式,原來(lái)的目的是為了EJB的分布式應(yīng)用提供粗粒度的數(shù)據(jù)實(shí)體,以減少分布式調(diào)用的次數(shù),從而提高分布式調(diào)用的性能和降低網(wǎng)絡(luò)負(fù)載,但在這里,我泛指用于展示層與服務(wù)層之間的數(shù)據(jù)傳輸對(duì)象。
DO(Domain Object):領(lǐng)域?qū)ο螅褪菑默F(xiàn)實(shí)世界中抽象出來(lái)的有形或無(wú)形的業(yè)務(wù)實(shí)體。
PO(Persistent Object):持久化對(duì)象,它跟持久層(通常是關(guān)系型數(shù)據(jù)庫(kù))的數(shù)據(jù)結(jié)構(gòu)形成一一對(duì)應(yīng)的映射關(guān)系,如果持久層是關(guān)系型數(shù)據(jù)庫(kù),那么,數(shù)據(jù)表中的每個(gè)字段(或若干個(gè))就對(duì)應(yīng)PO的一個(gè)(或若干個(gè))屬性。
模型:
下面以一個(gè)時(shí)序圖建立簡(jiǎn)單模型來(lái)描述上述對(duì)象在三層架構(gòu)應(yīng)用中的位置
l 用戶發(fā)出請(qǐng)求(可能是填寫表單),表單的數(shù)據(jù)在展示層被匹配為VO。
l 展示層把VO轉(zhuǎn)換為服務(wù)層對(duì)應(yīng)方法所要求的DTO,傳送給服務(wù)層。
l 服務(wù)層首先根據(jù)DTO的數(shù)據(jù)構(gòu)造(或重建)一個(gè)DO,調(diào)用DO的業(yè)務(wù)方法完成具體業(yè)務(wù)。
l 服務(wù)層把DO轉(zhuǎn)換為持久層對(duì)應(yīng)的PO(可以使用ORM工具,也可以不用),調(diào)用持久層的持久化方法,把PO傳遞給它,完成持久化操作。
l 對(duì)于一個(gè)逆向操作,如讀取數(shù)據(jù),也是用類似的方式轉(zhuǎn)換和傳遞,略。
VO與DTO的區(qū)別
大家可能會(huì)有個(gè)疑問(wèn)(在筆者參與的項(xiàng)目中,很多程序員也有相同的疑惑):既然DTO是展示層與服務(wù)層之間傳遞數(shù)據(jù)的對(duì)象,為什么還需要一個(gè)VO呢?對(duì)!對(duì)于絕大部分的應(yīng)用場(chǎng)景來(lái)說(shuō),DTO和VO的屬性值基本是一致的,而且他們通常都是POJO,因此沒(méi)必要多此一舉,但不要忘記這是實(shí)現(xiàn)層面的思維,對(duì)于設(shè)計(jì)層面來(lái)說(shuō),概念上還是應(yīng)該存在VO和DTO,因?yàn)閮烧哂兄举|(zhì)的區(qū)別,DTO代表服務(wù)層需要接收的數(shù)據(jù)和返回的數(shù)據(jù),而VO代表展示層需要顯示的數(shù)據(jù)。
用一個(gè)例子來(lái)說(shuō)明可能會(huì)比較容易理解:例如服務(wù)層有一個(gè)getUser的方法返回一個(gè)系統(tǒng)用戶,其中有一個(gè)屬性是gender(性別),對(duì)于服務(wù)層來(lái)說(shuō),它只從語(yǔ)義上定義:1-男性,2-女性,0-未指定,而對(duì)于展示層來(lái)說(shuō),它可能需要用“帥哥”代表男性,用“美女”代表女性,用“秘密”代表未指定。說(shuō)到這里,可能你還會(huì)反駁,在服務(wù)層直接就返回“帥哥美女”不就行了嗎?對(duì)于大部分應(yīng)用來(lái)說(shuō),這不是問(wèn)題,但設(shè)想一下,如果需求允許客戶可以定制風(fēng)格,而不同風(fēng)格對(duì)于“性別”的表現(xiàn)方式不一樣,又或者這個(gè)服務(wù)同時(shí)供多個(gè)客戶端使用(不同門戶),而不同的客戶端對(duì)于表現(xiàn)層的要求有所不同,那么,問(wèn)題就來(lái)了。再者,回到設(shè)計(jì)層面上分析,從職責(zé)單一原則來(lái)看,服務(wù)層只負(fù)責(zé)業(yè)務(wù),與具體的表現(xiàn)形式無(wú)關(guān),因此,它返回的DTO,不應(yīng)該出現(xiàn)與表現(xiàn)形式的耦合。
理論歸理論,這到底還是分析設(shè)計(jì)層面的思維,是否在實(shí)現(xiàn)層面必須這樣做呢?一刀切的做法往往會(huì)得不償失,下面我馬上會(huì)分析應(yīng)用中如何做出正確的選擇。
VO與DTO的應(yīng)用
上面只是用了一個(gè)簡(jiǎn)單的例子來(lái)說(shuō)明VO與DTO在概念上的區(qū)別,本節(jié)將會(huì)告訴你如何在應(yīng)用中做出正確的選擇。
在以下才場(chǎng)景中,我們可以考慮把VO與DTO二合為一(注意:是實(shí)現(xiàn)層面):
l 當(dāng)需求非常清晰穩(wěn)定,而且客戶端很明確只有一個(gè)的時(shí)候,沒(méi)有必要把VO和DTO區(qū)分開來(lái),這時(shí)候VO可以退隱,用一個(gè)DTO即可,為什么是VO退隱而不是DTO?回到設(shè)計(jì)層面,服務(wù)層的職責(zé)依然不應(yīng)該與展示層耦合,所以,對(duì)于前面的例子,你很容易理解,DTO對(duì)于“性別”來(lái)說(shuō),依然不能用“帥哥美女”,這個(gè)轉(zhuǎn)換應(yīng)該依賴于頁(yè)面的腳本(如JavaScript)或其他機(jī)制(JSTL、EL、CSS)
l 即使客戶端可以進(jìn)行定制,或者存在多個(gè)不同的客戶端,如果客戶端能夠用某種技術(shù)(腳本或其他機(jī)制)實(shí)現(xiàn)轉(zhuǎn)換,同樣可以讓VO退隱
以下場(chǎng)景需要優(yōu)先考慮VO、DTO并存:
l 上述場(chǎng)景的反面場(chǎng)景
l 因?yàn)槟撤N技術(shù)原因,比如某個(gè)框架(如Flex)提供自動(dòng)把POJO轉(zhuǎn)換為UI中某些Field時(shí),可以考慮在實(shí)現(xiàn)層面定義出VO,這個(gè)權(quán)衡完全取決于使用框架的自動(dòng)轉(zhuǎn)換能力帶來(lái)的開發(fā)和維護(hù)效率提升與設(shè)計(jì)多一個(gè)VO所多做的事情帶來(lái)的開發(fā)和維護(hù)效率的下降之間的比對(duì)。
l 如果頁(yè)面出現(xiàn)一個(gè)“大視圖”,而組成這個(gè)大視圖的所有數(shù)據(jù)需要調(diào)用多個(gè)服務(wù),返回多個(gè)DTO來(lái)組裝(當(dāng)然,這同樣可以通過(guò)服務(wù)層提供一次性返回一個(gè)大視圖的DTO來(lái)取代,但在服務(wù)層提供一個(gè)這樣的方法是否合適,需要在設(shè)計(jì)層面進(jìn)行權(quán)衡)。
DTO與DO的區(qū)別
首先是概念上的區(qū)別,DTO是展示層和服務(wù)層之間的數(shù)據(jù)傳輸對(duì)象(可以認(rèn)為是兩者之間的協(xié)議),而DO是對(duì)現(xiàn)實(shí)世界各種業(yè)務(wù)角色的抽象,這就引出了兩者在數(shù)據(jù)上的區(qū)別,例如UserInfo和User(對(duì)于DTO和DO的命名規(guī)則,請(qǐng)參見筆者前面的一篇博文),對(duì)于一個(gè)getUser方法來(lái)說(shuō),本質(zhì)上它永遠(yuǎn)不應(yīng)該返回用戶的密碼,因此UserInfo至少比User少一個(gè)password的數(shù)據(jù)。而在領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)中,正如第一篇系列文章所說(shuō),DO不是簡(jiǎn)單的POJO,它具有領(lǐng)域業(yè)務(wù)邏輯。
DTO與DO的應(yīng)用
從上一節(jié)的例子中,細(xì)心的讀者可能會(huì)發(fā)現(xiàn)問(wèn)題:既然getUser方法返回的UserInfo不應(yīng)該包含password,那么就不應(yīng)該存在password這個(gè)屬性定義,但如果同時(shí)有一個(gè)createUser的方法,傳入的UserInfo需要包含用戶的password,怎么辦?在設(shè)計(jì)層面,展示層向服務(wù)層傳遞的DTO與服務(wù)層返回給展示層的DTO在概念上是不同的,但在實(shí)現(xiàn)層面,我們通常很少會(huì)這樣做(定義兩個(gè)UserInfo,甚至更多),因?yàn)檫@樣做并不見得很明智,我們完全可以設(shè)計(jì)一個(gè)完全兼容的DTO,在服務(wù)層接收數(shù)據(jù)的時(shí)候,不該由展示層設(shè)置的屬性(如訂單的總價(jià)應(yīng)該由其單價(jià)、數(shù)量、折扣等決定),無(wú)論展示層是否設(shè)置,服務(wù)層都一概忽略,而在服務(wù)層返回?cái)?shù)據(jù)時(shí),不該返回的數(shù)據(jù)(如用戶密碼),就不設(shè)置對(duì)應(yīng)的屬性。
對(duì)于DO來(lái)說(shuō),還有一點(diǎn)需要說(shuō)明:為什么不在服務(wù)層中直接返回DO呢?這樣可以省去DTO的編碼和轉(zhuǎn)換工作,原因如下:
l 兩者在本質(zhì)上的區(qū)別可能導(dǎo)致彼此并不一一對(duì)應(yīng),一個(gè)DTO可能對(duì)應(yīng)多個(gè)DO,反之亦然,甚至兩者存在多對(duì)多的關(guān)系。
l DO具有一些不應(yīng)該讓展示層知道的數(shù)據(jù)
l DO具有業(yè)務(wù)方法,如果直接把DO傳遞給展示層,展示層的代碼就可以繞過(guò)服務(wù)層直接調(diào)用它不應(yīng)該訪問(wèn)的操作,對(duì)于基于AOP攔截服務(wù)層來(lái)進(jìn)行訪問(wèn)控制的機(jī)制來(lái)說(shuō),這問(wèn)題尤為突出,而在展示層調(diào)用DO的業(yè)務(wù)方法也會(huì)因?yàn)槭聞?wù)的問(wèn)題,讓事務(wù)難以控制。
l 對(duì)于某些ORM框架(如Hibernate)來(lái)說(shuō),通常會(huì)使用“延遲加載”技術(shù),如果直接把DO暴露給展示層,對(duì)于大部分情況,展示層不在事務(wù)范圍之內(nèi)(Open session in view在大部分情況下不是一種值得推崇的設(shè)計(jì)),如果其嘗試在Session關(guān)閉的情況下獲取一個(gè)未加載的關(guān)聯(lián)對(duì)象,會(huì)出現(xiàn)運(yùn)行時(shí)異常(對(duì)于Hibernate來(lái)說(shuō),就是LazyInitiliaztionException)。
l 從設(shè)計(jì)層面來(lái)說(shuō),展示層依賴于服務(wù)層,服務(wù)層依賴于領(lǐng)域?qū)樱绻?/span>DO暴露出去,就會(huì)導(dǎo)致展示層直接依賴于領(lǐng)域?qū)樱@雖然依然是單向依賴,但這種跨層依賴會(huì)導(dǎo)致不必要的耦合。
對(duì)于DTO來(lái)說(shuō),也有一點(diǎn)必須進(jìn)行說(shuō)明,就是DTO應(yīng)該是一個(gè)“扁平的二維對(duì)象”,舉個(gè)例子來(lái)說(shuō)明:如果User會(huì)關(guān)聯(lián)若干個(gè)其他實(shí)體(例如Address、Account、Region等),那么getUser()返回的UserInfo,是否就需要把其關(guān)聯(lián)的對(duì)象的DTO都一并返回呢?如果這樣的話,必然導(dǎo)致數(shù)據(jù)傳輸量的大增,對(duì)于分布式應(yīng)用來(lái)說(shuō),由于涉及數(shù)據(jù)在網(wǎng)絡(luò)上的傳輸、序列化和反序列化,這種設(shè)計(jì)更不可接受。如果getUser除了要返回User的基本信息外,還需要返回一個(gè)AccountId、AccountName、RegionId、RegionName,那么,請(qǐng)把這些屬性定義到UserInfo中,把一個(gè)“立體”的對(duì)象樹“壓扁”成一個(gè)“扁平的二維對(duì)象”,筆者目前參與的項(xiàng)目是一個(gè)分布式系統(tǒng),該系統(tǒng)不管三七二十一,把一個(gè)對(duì)象的所有關(guān)聯(lián)對(duì)象都轉(zhuǎn)換為相同結(jié)構(gòu)的DTO對(duì)象樹并返回,導(dǎo)致性能非常的慢。
DO與PO的區(qū)別
DO和PO在絕大部分情況下是一一對(duì)應(yīng)的,PO是只含有get/set方法的POJO,但某些場(chǎng)景還是能反映出兩者在概念上存在本質(zhì)的區(qū)別:
l DO在某些場(chǎng)景下不需要進(jìn)行顯式的持久化,例如利用策略模式設(shè)計(jì)的商品折扣策略,會(huì)衍生出折扣策略的接口和不同折扣策略實(shí)現(xiàn)類,這些折扣策略實(shí)現(xiàn)類可以算是DO,但它們只駐留在靜態(tài)內(nèi)存,不需要持久化到持久層,因此,這類DO是不存在對(duì)應(yīng)的PO的。
l 同樣的道理,某些場(chǎng)景下,PO也沒(méi)有對(duì)應(yīng)的DO,例如老師Teacher和學(xué)生Student存在多對(duì)多的關(guān)系,在關(guān)系數(shù)據(jù)庫(kù)中,這種關(guān)系需要表現(xiàn)為一個(gè)中間表,也就對(duì)應(yīng)有一個(gè)TeacherAndStudentPO的PO,但這個(gè)PO在業(yè)務(wù)領(lǐng)域沒(méi)有任何現(xiàn)實(shí)的意義,它完全不能與任何DO對(duì)應(yīng)上。這里要特別聲明,并不是所有多對(duì)多關(guān)系都沒(méi)有業(yè)務(wù)含義,這跟具體業(yè)務(wù)場(chǎng)景有關(guān),例如:兩個(gè)PO之間的關(guān)系會(huì)影響具體業(yè)務(wù),并且這種關(guān)系存在多種類型,那么這種多對(duì)多關(guān)系也應(yīng)該表現(xiàn)為一個(gè)DO,又如:“角色”與“資源”之間存在多對(duì)多關(guān)系,而這種關(guān)系很明顯會(huì)表現(xiàn)為一個(gè)DO——“權(quán)限”。
l 某些情況下,為了某種持久化策略或者性能的考慮,一個(gè)PO可能對(duì)應(yīng)多個(gè)DO,反之亦然。例如客戶Customer有其聯(lián)系信息Contacts,這里是兩個(gè)一對(duì)一關(guān)系的DO,但可能出于性能的考慮(極端情況,權(quán)作舉例),為了減少數(shù)據(jù)庫(kù)的連接查詢操作,把Customer和Contacts兩個(gè)DO數(shù)據(jù)合并到一張數(shù)據(jù)表中。反過(guò)來(lái),如果一本圖書Book,有一個(gè)屬性是封面cover,但該屬性是一副圖片的二進(jìn)制數(shù)據(jù),而某些查詢操作不希望把cover一并加載,從而減輕磁盤IO開銷,同時(shí)假設(shè)ORM框架不支持屬性級(jí)別的延遲加載,那么就需要考慮把cover獨(dú)立到一張數(shù)據(jù)表中去,這樣就形成一個(gè)DO對(duì)應(yīng)對(duì)個(gè)PO的情況。
l PO的某些屬性值對(duì)于DO沒(méi)有任何意義,這些屬性值可能是為了解決某些持久化策略而存在的數(shù)據(jù),例如為了實(shí)現(xiàn)“樂(lè)觀鎖”,PO存在一個(gè)version的屬性,這個(gè)version對(duì)于DO來(lái)說(shuō)是沒(méi)有任何業(yè)務(wù)意義的,它不應(yīng)該在DO中存在。同理,DO中也可能存在不需要持久化的屬性。
DO與PO的應(yīng)用
由于ORM框架的功能非常強(qiáng)大而大行其道,而且JavaEE也推出了JPA規(guī)范,現(xiàn)在的業(yè)務(wù)應(yīng)用開發(fā),基本上不需要區(qū)分DO與PO,PO完全可以通過(guò)JPA,Hibernate Annotations/hbm隱藏在DO之中。雖然如此,但有些問(wèn)題我們還必須注意:
l 對(duì)于DO中不需要持久化的屬性,需要通過(guò)ORM顯式的聲明,如:在JPA中,可以利用@Transient聲明。
l 對(duì)于PO中為了某種持久化策略而存在的屬性,例如version,由于DO、PO合并了,必須在DO中聲明,但由于這個(gè)屬性對(duì)DO是沒(méi)有任何業(yè)務(wù)意義的,需要讓該屬性對(duì)外隱藏起來(lái),最常見的做法是把該屬性的get/set方法私有化,甚至不提供get/set方法,但對(duì)于Hibernate來(lái)說(shuō),這需要特別注意,由于Hibernate從數(shù)據(jù)庫(kù)讀取數(shù)據(jù)轉(zhuǎn)換為DO時(shí),是利用反射機(jī)制先調(diào)用DO的空參數(shù)構(gòu)造函數(shù)構(gòu)造DO實(shí)例,然后再利用JavaBean的規(guī)范反射出set方法來(lái)為每個(gè)屬性設(shè)值,如果不顯式聲明set方法,或把set方法設(shè)置為private,都會(huì)導(dǎo)致Hibernate無(wú)法初始化DO,從而出現(xiàn)運(yùn)行時(shí)異常,可行的做法是把屬性的set方法設(shè)置為protected。
l 對(duì)于一個(gè)DO對(duì)應(yīng)多個(gè)PO,或者一個(gè)PO對(duì)應(yīng)多個(gè)DO的場(chǎng)景,以及屬性級(jí)別的延遲加載,Hibernate都提供了很好的支持,請(qǐng)參考Hibnate的相關(guān)資料。
來(lái)到**城后,新”家”有個(gè)很大的窗,外面是防盜網(wǎng),于是從家里帶來(lái)了兩顆田七苗,由于春天和向南的緣故,兩顆小苗長(zhǎng)得很快,每天都見它們向上攀爬了一大 截。兩天前,發(fā)現(xiàn)它們爬到防盜網(wǎng)頂了,防盜網(wǎng)頂部是封閉的,它們沒(méi)有了向上發(fā)展的空間,長(zhǎng)出一截吊在半空。今晚澆水的時(shí)候,我突然發(fā)現(xiàn)吊在半空的嫩枝都凋 謝了,我終于明白了原因。田七非常聰明,當(dāng)發(fā)現(xiàn)“前面是絕路”的時(shí)候,它會(huì)很明智的選擇放棄。種過(guò)田七的人會(huì)知道,田七雖然是攀藤植物,但它是可以長(zhǎng)出分 支的,而有些種植常識(shí)的人會(huì)知道,把生長(zhǎng)過(guò)長(zhǎng)的枝莖進(jìn)行修剪,會(huì)讓養(yǎng)分更充足,從而促進(jìn)主莖中長(zhǎng)出分支,形成更好的植株形狀。我相信田七“放棄”的原因也 正在于此,相信很快它們就會(huì)長(zhǎng)出分支來(lái)。
這個(gè)小小的發(fā)現(xiàn)其實(shí)蘊(yùn)藏著一種顯淺的生活智慧,這種智慧對(duì)于很多生物都是與生俱來(lái)的,但對(duì)于人來(lái)說(shuō),反而很多人都不懂,又或者做不到。領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)系列文章(1)——通過(guò)現(xiàn)實(shí)例子顯示領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)的威力
曾經(jīng)參與過(guò)系統(tǒng)維護(hù)或是在現(xiàn)有系統(tǒng)中進(jìn)行迭代開發(fā)的軟件工程師們,你們是否有過(guò)這樣的痛苦經(jīng)歷:當(dāng)需要修改一個(gè)Bug的時(shí)候,面對(duì)一個(gè)類中成百上千行的代碼,沒(méi)有注釋,千奇百怪的方法和變量名字,層層嵌套的方法調(diào)用,混亂不堪的結(jié)構(gòu),不要說(shuō)準(zhǔn)確找到Bug所在的位置,就是要清晰知道一段代碼究竟是做了什么也非常困難,最終,改對(duì)了一個(gè)Bug,卻多冒出N個(gè)新Bug;同樣的情況,當(dāng)你拿到一份新的需求,需要在現(xiàn)有系統(tǒng)中添加功能的時(shí)候,面對(duì)一行行完全過(guò)程式的代碼,需要使用一個(gè)功能時(shí),不知道是應(yīng)該自己編寫,還是應(yīng)該尋找是否已經(jīng)存在的方法,編寫一個(gè)非常簡(jiǎn)單的新、刪、改功能,卻要費(fèi)盡九牛二虎之力,最終發(fā)現(xiàn),系統(tǒng)存在著太多的重復(fù)邏輯,閱讀、測(cè)試、修改非常困難。在經(jīng)歷了這些痛苦之后,你們是否會(huì)不約而同的發(fā)出一個(gè)感慨:與其進(jìn)行系統(tǒng)維護(hù)和迭代開發(fā),還不如重新設(shè)計(jì)開發(fā)一個(gè)新的系統(tǒng)來(lái)得痛快?
面對(duì)這一系列讓軟件嵌入無(wú)底泥潭的問(wèn)題,基于面向?qū)ο笏枷氲念I(lǐng)域驅(qū)動(dòng)設(shè)計(jì)方法是一個(gè)很好的解決方法。從事過(guò)系統(tǒng)設(shè)計(jì)的富有經(jīng)驗(yàn)的設(shè)計(jì)師們,對(duì)職責(zé)單一原則、信息專家、充血/貧血模型、模型驅(qū)動(dòng)設(shè)計(jì)這些名詞或概念應(yīng)該不會(huì)感到陌生。面向?qū)ο蟮脑O(shè)計(jì)大師Martin Fowler不止一次的在他的Blog和著作《企業(yè)應(yīng)用架構(gòu)模式》中倡導(dǎo)過(guò)上述概論在設(shè)計(jì)中的巨大威力,而另外一位領(lǐng)域模型的出色專家Eric Evans的著作《領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)》也為我們提供了不少寶貴的經(jīng)驗(yàn)和方法。
筆者從事系統(tǒng)設(shè)計(jì)多年,將會(huì)在本系列文章中把本人對(duì)領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)的理解,結(jié)合工作過(guò)程中積累的實(shí)際項(xiàng)目經(jīng)驗(yàn)進(jìn)行淺析,希望與大家交流學(xué)習(xí)。
在本系列博文的開篇中,我將會(huì)拿出一個(gè)顯示的例子,先用傳統(tǒng)的面向過(guò)程方式,使用貧血模型進(jìn)行設(shè)計(jì),然后再逐步加入需求變更,讓讀者發(fā)現(xiàn),隨著系統(tǒng)的不斷變更,基于貧血模型的設(shè)計(jì)將會(huì)讓系統(tǒng)慢慢陷入泥潭,越來(lái)越難于維護(hù),然后再用基于面向?qū)ο蟮念I(lǐng)域驅(qū)動(dòng)設(shè)計(jì)重新上述過(guò)程,通過(guò)對(duì)比展示領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)對(duì)于復(fù)雜的業(yè)務(wù)系統(tǒng)的威力。
假設(shè)現(xiàn)在有一個(gè)銀行支付系統(tǒng)項(xiàng)目,其中的一個(gè)重要的業(yè)務(wù)用例是賬戶轉(zhuǎn)賬業(yè)務(wù)。系統(tǒng)使用迭代的方式進(jìn)行開發(fā),在1.0版本中,該用例的功能需求非常簡(jiǎn)單,事件流描述如下:
主事件流:
1) 用戶登錄銀行的在線支付系統(tǒng)
2) 選擇用戶在該銀行注冊(cè)的網(wǎng)上銀行賬戶
3) 選擇需要轉(zhuǎn)賬的目標(biāo)賬戶,輸入轉(zhuǎn)賬金額,申請(qǐng)轉(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)賬失敗,返回錯(cuò)誤信息
面向過(guò)程的設(shè)計(jì)方式(貧血模型)
設(shè)計(jì)方案如下(忽略展示層部分):
1) 設(shè)計(jì)一個(gè)賬戶交易服務(wù)接口AccountingService,設(shè)計(jì)一個(gè)服務(wù)方法transfer(),并提供一個(gè)具體實(shí)現(xiàn)類AccountingServiceImpl,所有賬戶交易業(yè)務(wù)的業(yè)務(wù)邏輯都置于該服務(wù)類中。
2) 提供一個(gè)AccountInfo和一個(gè)Account,前者是一個(gè)用于與展示層交換賬戶數(shù)據(jù)的賬戶數(shù)據(jù)傳輸對(duì)象,后者是一個(gè)賬戶實(shí)體(相當(dāng)于一個(gè)EntityBean),這兩個(gè)對(duì)象都是普通的JavaBean,具有相關(guān)屬性和簡(jiǎn)單的get/set方法。
下面是AccountingServiceImpl.transfer()方法的實(shí)現(xiàn)邏輯(偽代碼):
可以看到,由于1.0版本的功能需求非常簡(jiǎn)單,按面向過(guò)程的設(shè)計(jì)方式,把所有業(yè)務(wù)代碼置于AccountingServiceImpl中完全沒(méi)有問(wèn)題。
這時(shí)候,新需求來(lái)了,在1.0.1版本中,需要為賬戶轉(zhuǎn)賬業(yè)務(wù)增加如下功能,在轉(zhuǎn)賬時(shí),首先需要判斷賬戶是否可用,然后,賬戶的余額還要分成兩部分:凍結(jié)部分和活躍部分,處于凍結(jié)部分的金額不能用于任何交易業(yè)務(wù),我們來(lái)看看變更后的代碼:
可以看到,情況變得稍微復(fù)雜了,這時(shí)候,1.0.2的需求又來(lái)了,需要在每次交易成功后,創(chuàng)建一個(gè)交易明細(xì)賬,于是,我們又必須在transfer()方面里面增加創(chuàng)建并持久化交易明細(xì)賬的業(yè)務(wù)邏輯:
業(yè)務(wù)需求不斷復(fù)雜化:賬戶每筆轉(zhuǎn)賬的最大額度需要由其信用指數(shù)確定、需要根據(jù)銀行的手續(xù)費(fèi)策略計(jì)算并扣除一定的手續(xù)費(fèi)用……,隨著業(yè)務(wù)的復(fù)雜化,transfer()方法的邏輯變得越來(lái)越復(fù)雜,逐漸形成了上文所述的成百上千行代碼。有經(jīng)驗(yàn)的程序員可能會(huì)做出類此“方法抽取”的重構(gòu),把轉(zhuǎn)賬業(yè)務(wù)按邏輯劃分成若干塊:判斷余額是否足夠、判斷賬戶的信用指數(shù)以確定每筆最大轉(zhuǎn)賬金額、根據(jù)銀行的手續(xù)費(fèi)策略計(jì)算手續(xù)費(fèi)、記錄交易明細(xì)賬……,從而使代碼更加結(jié)構(gòu)化。這是一個(gè)好的開始,但還是顯然不足。
假設(shè)某一天,系統(tǒng)需求增加一個(gè)新的模塊,為系統(tǒng)增加一個(gè)網(wǎng)上商城,讓銀行用戶可以進(jìn)行在線購(gòu)物,而在線購(gòu)物也存在著很多與賬戶貸記借記業(yè)務(wù)相同或相似的業(yè)務(wù)邏輯:判斷余額是否足夠、對(duì)賬戶進(jìn)行借貸操作(credit/debit)以改變余額、收取手續(xù)費(fèi)用、產(chǎn)生交易明細(xì)賬……
面對(duì)這種情況,有兩種解決辦法:
1) 把AccountingServiceImpl中的相同邏輯拷貝到OnlineShoppingServiceImplementation中
2) 讓OnlineShoppingServiceImpl調(diào)用AccountingServiceImpl的相同服務(wù)
顯然,第二種方法比第一種方法更好,結(jié)構(gòu)更清晰,維護(hù)更容易。但問(wèn)題在于,這樣就會(huì)形成網(wǎng)上商城服務(wù)模塊與賬戶收支服務(wù)模塊的不必要的依賴關(guān)系,系統(tǒng)的耦合度高了,如果系統(tǒng)為了更靈活的伸縮性,讓每個(gè)大業(yè)務(wù)模塊獨(dú)立進(jìn)行部署,還需要因?yàn)閮烧叩囊蕾囮P(guān)系建立分布式調(diào)用,這無(wú)疑增加了設(shè)計(jì)、開發(fā)和運(yùn)維的成本。
有經(jīng)驗(yàn)的設(shè)計(jì)人員可能會(huì)發(fā)現(xiàn)第三種解決辦法:把相同的業(yè)務(wù)邏輯抽取成一個(gè)新的服務(wù),作為公共服務(wù)同時(shí)供上述兩個(gè)業(yè)務(wù)模塊使用。這只是筆者將會(huì)馬上討論的方案——使用領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)。
面向過(guò)程的領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)方式(充血模型)
為了節(jié)省篇幅,這里就直接以最復(fù)雜的業(yè)務(wù)需求來(lái)進(jìn)行設(shè)計(jì)。
領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)的一個(gè)重要的概念是領(lǐng)域模型,首先,我們根據(jù)業(yè)務(wù)領(lǐng)域抽象出以下核心業(yè)務(wù)對(duì)象模型:
Account:賬戶,是整個(gè)系統(tǒng)的最核心的業(yè)務(wù)對(duì)象,它包括以下屬性:對(duì)象標(biāo)識(shí)、賬戶號(hào)、是否有效標(biāo)識(shí)、余額、凍結(jié)金額、賬戶交易明細(xì)集合、賬戶信用等級(jí)。
AccountTransactionDetails:賬戶交易明細(xì),它從屬于賬戶,每個(gè)賬戶有多個(gè)交易明細(xì),它包括以下屬性:對(duì)象標(biāo)識(shí)、所屬賬戶、交易類型、交易發(fā)生金額、交易發(fā)生時(shí)間。
AccountCreditDegree:賬戶信用等級(jí),它用于限制賬戶的每筆交易發(fā)生金額,包含以下屬性:對(duì)象標(biāo)識(shí)、對(duì)應(yīng)賬戶、信用指數(shù)。
BankTransactionFeeCalculator:銀行交易手續(xù)費(fèi)用計(jì)算器,它包含一個(gè)常量:每筆交易的手續(xù)費(fèi)上限。
我們知道,領(lǐng)域?qū)ο蟪司哂凶陨淼膶傩院蜖顟B(tài)之外,它的一個(gè)很重要的標(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è)計(jì)業(yè)務(wù)方法:
根據(jù)職責(zé)單一的原則,我們把功能需求中描述的功能合理的分配到不同的領(lǐng)域?qū)ο笾校?/span>
Account:
(我們可以看到,后兩個(gè)業(yè)務(wù)方法被聲明為protected,具體原因見后述)
AccountCreditDegree:
BankTransactionFeeCalculator:
經(jīng)過(guò)這樣的設(shè)計(jì),前例中所有放置在服務(wù)對(duì)象的業(yè)務(wù)邏輯被分別劃入不同的負(fù)責(zé)相關(guān)職責(zé)的領(lǐng)域?qū)ο螽?dāng)中,下面的時(shí)序圖描述了AccountingServiceImpl的轉(zhuǎn)賬業(yè)務(wù)的實(shí)現(xiàn)邏輯(為了簡(jiǎn)化邏輯,我們忽略掉事物、持久化等邏輯):
再看看AccountingServiceImpl.transfer()的實(shí)現(xiàn)邏輯:
我們可以看到,上例那些復(fù)雜的業(yè)務(wù)邏輯:判斷余額是否足夠、判斷賬戶是否可用、改變賬戶余額、計(jì)算手續(xù)費(fèi)、判斷交易額度、產(chǎn)生交易明細(xì)賬……,都不再存在于AccountingServiceImplementation的transfer方法中,它們被委派給負(fù)責(zé)這些業(yè)務(wù)的領(lǐng)域?qū)ο蟮臉I(yè)務(wù)方法中去,現(xiàn)在應(yīng)該猜到為什么Account中有兩個(gè)方法被聲明為protected了吧,因?yàn)樗麄兪窃?/span>debit和credit方法被調(diào)用時(shí),由這兩個(gè)方法調(diào)用的,對(duì)于AccountingServiceImpl來(lái)說(shuō),由于產(chǎn)生交易明細(xì)(createTransactionDetails)和更新賬戶信用指數(shù)(updateCreditIndex)都不屬于其職責(zé)范圍,它不需要也無(wú)權(quán)使用這些邏輯。
我們可以看到,使用領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)至少會(huì)帶來(lái)下述優(yōu)點(diǎn):
再看看如果這時(shí)需要加入網(wǎng)上商城的一個(gè)新的模塊,開發(fā)人員需要怎么去做,還記得上面提過(guò)的第三種方案嗎?就是把賬戶貸記和借記的相關(guān)業(yè)務(wù)抽取到成一個(gè)公共服務(wù),同時(shí)供銀行在線支付系統(tǒng)和網(wǎng)上商城系統(tǒng)服務(wù),其實(shí)這個(gè)公共的服務(wù),本質(zhì)上就是這些具有領(lǐng)域邏輯的領(lǐng)域?qū)ο螅?/span>Account、AccountCreditDegree……,由此我們又可以發(fā)現(xiàn)領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)的一大優(yōu)點(diǎn):
筆者經(jīng)驗(yàn)尚淺,而且文筆拙劣,希望通過(guò)這樣的一個(gè)場(chǎng)景的分析比較,能讓讀者初步認(rèn)識(shí)到基于面向?qū)ο蟮念I(lǐng)域驅(qū)動(dòng)設(shè)計(jì)的威力,并在實(shí)際項(xiàng)目中嘗試應(yīng)用。本篇是領(lǐng)取驅(qū)動(dòng)設(shè)計(jì)系列博文的第一篇,在系列文章的第二篇博文中,筆者將會(huì)淺析VO、DTO、DO、PO的概念、用處和區(qū)別,敬請(qǐng)各位對(duì)本系列博文感興趣的讀者關(guān)注并給予指導(dǎo)修正。
的相信外部傳入的數(shù)據(jù),不進(jìn)行校驗(yàn),導(dǎo)致被我發(fā)現(xiàn)了一個(gè)致命的漏洞,從而測(cè)試性的盜取了2分錢,這個(gè)事情一直到2個(gè)月后,該平臺(tái)通過(guò)
與網(wǎng)銀對(duì)賬才發(fā)現(xiàn),由于不知道這個(gè)致命的漏洞是否已經(jīng)修復(fù),避免有惡意的人使用該方式去做違法的事情,我在這里只簡(jiǎn)單描述該漏洞,
而不公開該支付平臺(tái)的真實(shí)身份。
該漏洞出現(xiàn)在充值業(yè)務(wù)上,實(shí)現(xiàn)邏輯請(qǐng)看下面的Sequence diagram:
描述一下流程:
1.客戶打開第三方支付平臺(tái)的充值頁(yè)面,選擇一個(gè)銀行,填入充值數(shù)據(jù)(充值虛擬賬戶、充值金額等),提交表單。
2.充值頁(yè)面把數(shù)據(jù)(包括充值數(shù)據(jù),第三方支付平臺(tái)在該銀行注冊(cè)的商戶標(biāo)識(shí))使用銀行提供的加密工具進(jìn)行加密,傳遞給指定銀行的網(wǎng)銀系統(tǒng)前臺(tái)。
3.網(wǎng)銀系統(tǒng)前臺(tái)產(chǎn)生一張轉(zhuǎn)賬訂單,并要求用戶輸入賬號(hào)、密碼,然后提交。
4.網(wǎng)銀系統(tǒng)后端校驗(yàn)賬號(hào)和密碼,然后根據(jù)用戶賬號(hào),充值金額,第三方支付平臺(tái)商戶標(biāo)識(shí),把金額從用戶賬號(hào)轉(zhuǎn)賬到第三方支付平臺(tái)賬號(hào)。
5.網(wǎng)銀系統(tǒng)把確認(rèn)數(shù)據(jù)(充值虛擬賬戶、充值金額、充值結(jié)果、轉(zhuǎn)入商戶標(biāo)識(shí)等)加密后重定向到第三方支付平臺(tái)結(jié)果頁(yè)面。
6.頁(yè)面把確認(rèn)數(shù)據(jù)提交給后臺(tái),并向客戶顯示充值成功。
7.第三方支付平臺(tái)后臺(tái)接收到確認(rèn)數(shù)據(jù),把虛擬金額充入虛擬賬戶,完成整個(gè)流程。
漏洞描述:
我們可以看到,用戶在第三方支付平臺(tái)充值頁(yè)面提交數(shù)據(jù),和網(wǎng)銀向第三方支付平臺(tái)發(fā)送確認(rèn)數(shù)據(jù),都需要對(duì)數(shù)據(jù)進(jìn)行非對(duì)稱加密,但在當(dāng)
時(shí),我發(fā)現(xiàn)某一銀行,竟然不提供發(fā)送充值數(shù)據(jù)時(shí)的加密工具,這意味著,選擇這家銀行,用戶必須以明文的格式提交充值數(shù)據(jù),好了,這
下我可以嘗試進(jìn)行man in the middle attack了,在我提交表單一刻,我把頁(yè)面的HTML腳本修改了,把提交時(shí)由第三方支付平臺(tái)指定的在所
選銀行注冊(cè)的商戶標(biāo)識(shí)修改為另外一個(gè)商戶的標(biāo)識(shí)(假設(shè)是我自己注冊(cè)的商戶),OK,這意味著我的充值金額從我的賬號(hào)轉(zhuǎn)到我修改后的注
冊(cè)商戶賬號(hào),這時(shí),該網(wǎng)銀把確認(rèn)數(shù)據(jù)(注意,包括轉(zhuǎn)入商戶標(biāo)識(shí),這時(shí)的商戶標(biāo)識(shí)是我修改后的標(biāo)識(shí))發(fā)送回第三方支付平臺(tái),其實(shí),如
果第三方支付平臺(tái)如果能進(jìn)一步校驗(yàn)確認(rèn)數(shù)據(jù)中的注冊(cè)商戶標(biāo)識(shí),就能發(fā)現(xiàn)注冊(cè)商戶標(biāo)識(shí)被惡意篡改,從而導(dǎo)致充值失敗,但該平臺(tái)的設(shè)計(jì)
人員卻假設(shè)之前的充值頁(yè)面已經(jīng)自動(dòng)填入了自己的注冊(cè)商戶標(biāo)識(shí),這里并不作校驗(yàn),從而導(dǎo)致這個(gè)致命的漏洞,錢沒(méi)有轉(zhuǎn)到自己的賬號(hào)上,
而自己卻把等額的虛擬資金充值到了第三方虛擬賬戶中。
前臺(tái)的查看頁(yè)面需要顯示這個(gè)值,當(dāng)用戶修改a.x或者a.b.x時(shí),要觸發(fā)一個(gè)事件,實(shí)時(shí)的在前臺(tái)更新resident,后臺(tái)需要以這個(gè)resident來(lái)
做某些業(yè)務(wù)邏輯的判斷。
我們知道,由于resident完全可以由a.x與a.b.x決定,屬于一個(gè)冗余數(shù)據(jù),不需要顯式的增加該屬性。很快,前臺(tái)的那位開發(fā)人員就輕松的
告訴我,這個(gè)任務(wù)已經(jīng)寫完了,原來(lái)他是在頁(yè)面寫了一個(gè)JS腳本,當(dāng)a.x或a.b.x發(fā)生改變時(shí),觸發(fā)一個(gè)onChange()事件,里面就實(shí)現(xiàn)了上述
的邏輯。我一看,感覺(jué)問(wèn)題來(lái)了,然后我告訴他,我已經(jīng)在后臺(tái)的服務(wù)提供了一個(gè)方法,用于判斷resident的值,他應(yīng)該在事件觸發(fā)的時(shí)候
,通過(guò)異步方式調(diào)用后臺(tái)方法來(lái)獲取reisdent的值。他一聽覺(jué)得非常奇怪,這么簡(jiǎn)單的邏輯,為什么需要使用異步方式去調(diào)用后臺(tái)這么麻煩
,于是我就從設(shè)計(jì)的角度跟他解釋,resident由 a.x與a.b.x決定,這是一個(gè)業(yè)務(wù)邏輯,從職責(zé)劃分的設(shè)計(jì)原則分析,前臺(tái)只負(fù)責(zé)顯示邏輯,
業(yè)務(wù)邏輯屬于后臺(tái)的職責(zé)。接著,他又提出了疑問(wèn),說(shuō)這里其實(shí)只有一行代碼,沒(méi)有必要分得那么清,這樣調(diào)用不但麻煩,而且性能相對(duì)較
低。
這個(gè)例子,反映出了很多開發(fā)人員的通病,沒(méi)有真正從事物的本質(zhì)考慮問(wèn)題,只從代碼的數(shù)量去考慮。
上述的做法存在兩個(gè)問(wèn)題:
1.開發(fā)人員沒(méi)有從職責(zé)劃分的角度考慮問(wèn)題,而只是貪圖一時(shí)的方便,或者覺(jué)得簡(jiǎn)單的一兩行代碼不需要進(jìn)行設(shè)計(jì),但實(shí)際上,上面的問(wèn)題
必然導(dǎo)致重復(fù)代碼。上面的例子,即使只有一行代碼,但在系統(tǒng)的前后臺(tái)卻出現(xiàn)了兩份相同的邏輯,假設(shè)以后某天業(yè)務(wù)邏輯發(fā)生了變化,那
么,必須在兩個(gè)地方(甚至更多的地方)進(jìn)行修改、測(cè)試,如果漏掉了一個(gè)地方,就會(huì)導(dǎo)致Bug的出現(xiàn)。作為一個(gè)開發(fā)人員,如果不注意這些
細(xì)節(jié),隨意的拷貝和重復(fù)代碼,長(zhǎng)期下來(lái),一個(gè)系統(tǒng)就會(huì)遍布無(wú)數(shù)的重復(fù)代碼,系統(tǒng)越往后期就越難維護(hù),最終陷于崩潰。
2.如果作為后臺(tái)開發(fā)人員,看到前臺(tái)已經(jīng)實(shí)現(xiàn)了這個(gè)邏輯,而在前臺(tái)往后臺(tái)傳遞數(shù)據(jù)的時(shí)候,把resident也傳遞進(jìn)來(lái),從而認(rèn)為后臺(tái)就不需
要重復(fù)這段邏輯,直接拿著前臺(tái)傳過(guò)來(lái)的這個(gè)resident來(lái)進(jìn)行業(yè)務(wù)判斷,那么就可能會(huì)給系統(tǒng)帶來(lái)致命的漏洞,因?yàn)閿?shù)據(jù)在傳輸過(guò)程中,很
可能被有意或無(wú)意的修改,不在后臺(tái)進(jìn)行業(yè)務(wù)規(guī)則校驗(yàn)這個(gè)錯(cuò)誤是常見的,但也是致命的。