如何選擇充血模型和貧血模型
從領(lǐng)域模型說(shuō)起
回顧一下我們進(jìn)行領(lǐng)域建模時(shí)候的流程:
- 進(jìn)行需求分析
- 進(jìn)行用例設(shè)計(jì)
- 針對(duì)用例進(jìn)行領(lǐng)域建模
- 針對(duì)領(lǐng)域模型并行進(jìn)行數(shù)據(jù)庫(kù)設(shè)計(jì)和程序設(shè)計(jì)。
在經(jīng)過(guò)了前面幾步分析后,我們會(huì)得到領(lǐng)域模型以及他們之間的關(guān)系。在這之后我們要根據(jù)領(lǐng)域模型分別進(jìn)行數(shù)據(jù)庫(kù)設(shè)計(jì)與程序設(shè)計(jì)。我們會(huì)根據(jù)領(lǐng)域模型之間的關(guān)系將模型之間的關(guān)系映射到系統(tǒng)表設(shè)計(jì)之間的關(guān)系。那么我們?cè)撛趺催M(jìn)行對(duì)應(yīng)的程序設(shè)計(jì)呢?
一般來(lái)說(shuō):
將領(lǐng)域模型設(shè)計(jì)轉(zhuǎn)化為程序設(shè)計(jì),有貧血模型與充血模型兩種方法。
貧血模型
在之前的文章中我們舉過(guò)一個(gè)訂單模型的例子:
在這個(gè)例子中我們經(jīng)過(guò)了設(shè)計(jì)和抽象得到了一個(gè)訂單模型。而根據(jù)這個(gè)模型,我們可以直接將模型中的屬性抽離成為一個(gè)模型設(shè)計(jì),將模型中的方法能力抽取為一個(gè)服務(wù)設(shè)計(jì)。也就是說(shuō):
只由屬性及其賦值器、取值器構(gòu)成的對(duì)象,我們稱(chēng)之為貧血模型。
那么如果使用貧血模型進(jìn)行領(lǐng)域模型的程序設(shè)計(jì),就會(huì)像上圖中的例子一樣,得到一個(gè)領(lǐng)域模型的實(shí)體對(duì)象與服務(wù)。實(shí)體對(duì)象將包含模型對(duì)象的所有屬性與數(shù)據(jù),服務(wù)中包含領(lǐng)域模型中的所有方法。當(dāng)我們希望使用領(lǐng)域模型中的方法時(shí),是將模型實(shí)體作為參數(shù)調(diào)用服務(wù)中領(lǐng)域模型對(duì)應(yīng)的方法。
在利用貧血模型建模后,原有的領(lǐng)域模型中的數(shù)據(jù)與方法被割裂到了兩個(gè)對(duì)象中。而這種割裂使得原本被封裝到一個(gè)對(duì)象中的內(nèi)容,被分開(kāi)到了兩個(gè)對(duì)象中。而打破了原本領(lǐng)域模型的封裝性。
封裝性的打破會(huì)帶來(lái)新的問(wèn)題,我舉一個(gè)我在實(shí)際生產(chǎn)中遇到過(guò)的例子:
如圖所示,如果對(duì)于訂單來(lái)說(shuō)我們出現(xiàn)了兩種子對(duì)象:入駐商家訂單、自營(yíng)訂單。對(duì)于這兩種訂單的下單方法中是有不一樣的業(yè)務(wù)流程的。如果使用貧血模型設(shè)計(jì)的話(huà),我們很容易就想到將兩個(gè)不一樣的下單業(yè)務(wù)流程分別通過(guò)兩個(gè)服務(wù)來(lái)實(shí)現(xiàn)。那么對(duì)應(yīng)的,為了區(qū)分服務(wù)的不同,就需要將訂單也分別分割為兩個(gè)對(duì)象,并且指定的對(duì)象作為入?yún)⒄{(diào)用對(duì)應(yīng)的服務(wù)。
而當(dāng)這樣設(shè)計(jì)了之后,在訂單上游還需要一層業(yè)務(wù)編排層來(lái)對(duì)訂單數(shù)據(jù)的流轉(zhuǎn)進(jìn)行處理,以保證對(duì)應(yīng)的對(duì)象不會(huì)錯(cuò)誤地進(jìn)入到。
按照這樣的設(shè)計(jì)思想,如果在在這個(gè)基礎(chǔ)上新增一個(gè)“分銷(xiāo)訂單”。我們就需要調(diào)整三個(gè)地方:新增分銷(xiāo)訂單對(duì)象、新增分銷(xiāo)訂單服務(wù)、調(diào)整上游編排層。顯然這不符合開(kāi)閉原則,同時(shí)業(yè)務(wù)開(kāi)發(fā)的成本也提高。
注:我們可以通過(guò)設(shè)計(jì)模式優(yōu)化最終實(shí)現(xiàn)的邏輯,但是設(shè)計(jì)思想是這樣的。
充血模型
貧血模型的問(wèn)題在于割裂了領(lǐng)域模型的封裝,那么不對(duì)模型的封裝進(jìn)行割裂,而是保留領(lǐng)域模型的原貌進(jìn)行程序設(shè)計(jì),就是充血模型??梢悦枋鰹椋?/p>
將領(lǐng)域模型中的方法直接在領(lǐng)域?qū)ο笾袑?shí)現(xiàn),就是充血模型設(shè)計(jì)。
根據(jù)上文中的訂單領(lǐng)域模型,如果我們換成充血模型設(shè)計(jì),就變?yōu)榱耍?/p>
可以看到,如果采用充血模型的設(shè)計(jì),訂單的實(shí)體對(duì)象中是包含領(lǐng)域模型中的方法的,也就是說(shuō)下單的實(shí)際邏輯由訂單對(duì)象自己完成。而對(duì)于訂單對(duì)象中的子對(duì)象,也僅需要繼承訂單對(duì)象,然后實(shí)現(xiàn)自己的下單方法即可。使用充血模型設(shè)計(jì)后我們發(fā)現(xiàn)仍然有訂單對(duì)應(yīng)的服務(wù),但是這個(gè)服務(wù)僅執(zhí)行一個(gè)調(diào)用訂單對(duì)象中對(duì)應(yīng)邏輯的作用,而這樣的服務(wù)是不關(guān)心具體調(diào)用的是哪一個(gè)子類(lèi)的。
所以,我們使用充血模型設(shè)計(jì),當(dāng)需要新增一個(gè)“分銷(xiāo)訂單”時(shí),就只需要?jiǎng)?chuàng)建一個(gè)訂單的新的子類(lèi),然后在類(lèi)中實(shí)現(xiàn)對(duì)應(yīng)的下單邏輯就可以了,而這是符合開(kāi)閉原則的。
而由于充血模型還原的是領(lǐng)域模型的原貌,所以在依據(jù)領(lǐng)域模型進(jìn)行程序設(shè)計(jì)的時(shí)候,其映射關(guān)系直觀(guān),所以對(duì)應(yīng)的代碼修改就更直接。
貧血模型VS充血模型
通過(guò)上文分析,充血模型是全面優(yōu)于貧血模型的。但是在實(shí)際的開(kāi)發(fā)應(yīng)用中并非如此。
貧血模型比充血模型實(shí)現(xiàn)更簡(jiǎn)單
由于充血模型是還原領(lǐng)域模型的原貌,所以在進(jìn)行程序的操作的時(shí)候,需要將模型下的所有組合、聚合對(duì)象都進(jìn)行組裝,以訂單為例,訂單需要:訂單、訂單明細(xì)、用戶(hù)、用戶(hù)地址、商品等5個(gè)對(duì)象進(jìn)行封裝。而由于封裝的復(fù)雜性,所以還需要設(shè)計(jì)訂單倉(cāng)庫(kù)、訂單工廠(chǎng)等組件用于創(chuàng)建訂單的對(duì)象。同時(shí)因?yàn)閯?chuàng)建后的對(duì)象大小可能會(huì)比較大,訂單倉(cāng)庫(kù)中一般還需要進(jìn)行緩存設(shè)計(jì)??偟膩?lái)說(shuō):
充血模型要依靠強(qiáng)大的技術(shù)平臺(tái)來(lái)維護(hù)模型的使用。
而因?yàn)樨氀P褪峭ㄟ^(guò)將模型之間進(jìn)行分割而實(shí)現(xiàn)的,所以當(dāng)操作訂單的時(shí)候,只需根據(jù)需要操作訂單、訂單明細(xì)就可以。而一般的三層設(shè)計(jì)Controller、Service、Dao就可以支撐起來(lái)訂單的模型設(shè)計(jì)。由于分割的模型數(shù)據(jù)不必組裝成領(lǐng)域模型對(duì)象,所以在Dao查詢(xún)完畢數(shù)據(jù)后,可以直接返回給Service進(jìn)行使用,反之亦然。系統(tǒng)的復(fù)雜性大大降低。
貧血模型簡(jiǎn)單直接,可在經(jīng)典三層中直接實(shí)現(xiàn)。
充血模型需要具備更強(qiáng)的設(shè)計(jì)能力
由于充血模型是對(duì)領(lǐng)域模型的直接表現(xiàn),所以領(lǐng)域模型設(shè)計(jì)的優(yōu)劣會(huì)直接影響到系統(tǒng)的整體性能與擴(kuò)展性。這就要求開(kāi)發(fā)人員有更強(qiáng)的對(duì)象分析、對(duì)象設(shè)計(jì)能力。同時(shí)由于充血模型中需要對(duì)不同領(lǐng)域中的數(shù)據(jù)進(jìn)行聚合,所以可能需要在團(tuán)隊(duì)之間提出數(shù)據(jù)查詢(xún)需求,也就需要有更強(qiáng)的團(tuán)隊(duì)協(xié)作能力。
而相反的,貧血模型所有業(yè)務(wù)處理都在service中進(jìn)行操作,且對(duì)模型進(jìn)行了分割,所以對(duì)于數(shù)據(jù)的外部依賴(lài)就更少,層級(jí)也更少。大部分邏輯都是直接Dao查詢(xún)數(shù)據(jù)庫(kù)后返回,對(duì)于開(kāi)發(fā)人員的能力要求就更小。
貧血模型可面向步驟編程
在面對(duì)長(zhǎng)串的復(fù)雜業(yè)務(wù)場(chǎng)景時(shí),我們可能更傾向于將業(yè)務(wù)拆解為多個(gè)串聯(lián)的步驟然后獨(dú)立執(zhí)行。而在使用貧血模型的情況下可以通過(guò)編寫(xiě)多個(gè)Service直接進(jìn)行面向步驟編程。每個(gè)Service可以處理對(duì)象中的部分?jǐn)?shù)據(jù),在通過(guò)多個(gè)Service處理后得到最終結(jié)果。
盡管充血模型也可以在方法中進(jìn)行拆分,但是并不如貧血模型來(lái)的直接。
貧血模型AND充血模型
盡管上文中對(duì)充血模型與貧血模型進(jìn)行了直接的區(qū)分,但是這兩種設(shè)計(jì)并非二元論的。簡(jiǎn)單來(lái)說(shuō),我們可以根據(jù)充血模型與貧血模型的特點(diǎn),選取我們需要的部分進(jìn)行使用。例如:
- 將需要封裝的業(yè)務(wù)邏輯到領(lǐng)域?qū)ο笾?,按照充血模型去設(shè)計(jì)
- 將不需要封裝的業(yè)務(wù)邏輯放到Service中,按照貧血模型去設(shè)計(jì)
那么針對(duì)上文中的訂單模型來(lái)說(shuō)設(shè)計(jì)就變成了:
也就是僅將需要封裝的下單邏輯放到了“入駐商家訂單”、“自營(yíng)訂單”中,而付款、查詢(xún)訂單狀態(tài)邏輯則仍然放到Service進(jìn)行主要處理。
事實(shí)上,需要封裝的這個(gè)概念也是比較模糊的,但是基本上可以參考:
- 存在繼承、多態(tài)關(guān)系:例如訂單與自營(yíng)訂單
- 需要編碼轉(zhuǎn)換:例如根據(jù)數(shù)值返回對(duì)應(yīng)的枚舉對(duì)象
- 需要體現(xiàn)出必要領(lǐng)域關(guān)聯(lián)性:例如訂單與訂單明細(xì)的關(guān)系。
最后
盡管經(jīng)過(guò)了合理的分析后,得到的結(jié)論是應(yīng)該根據(jù)特性進(jìn)行貧血和充血模型的混用。但是在實(shí)際企業(yè)中,如何評(píng)判哪些方法可以放到模型中的這個(gè)標(biāo)準(zhǔn)是相對(duì)模糊的。也就是說(shuō)更多的還是需要依靠架構(gòu)設(shè)計(jì)、開(kāi)發(fā)的個(gè)人能力、代碼review,而上述三個(gè)都對(duì)小團(tuán)隊(duì)、年輕團(tuán)隊(duì)不是很友好。所以我認(rèn)為如果為了提高下限,則使用貧血模型更加穩(wěn)妥。