教會你何時(shí)定義領(lǐng)域服務(wù)
若遵循基于面向?qū)ο笤O(shè)計(jì)范式的領(lǐng)域驅(qū)動設(shè)計(jì),并用以應(yīng)對紛繁復(fù)雜的業(yè)務(wù)邏輯,則強(qiáng)調(diào)領(lǐng)域模型的充血設(shè)計(jì)模型已成為社區(qū)不爭事實(shí)。我將Eric提及的戰(zhàn)術(shù)設(shè)計(jì)要素如Entity、Value Object、Domain Service、Aggregate、Repository與Factory視為設(shè)計(jì)模型。這其中,只有Entity、Value Object和Domain Service才能表達(dá)領(lǐng)域邏輯。
為避免貧血模型,在封裝領(lǐng)域邏輯時(shí),考慮設(shè)計(jì)要素的順序?yàn)椋?/p>
- Value Object -> Entity -> Domain Service
切記,我們必須將Domain Service作為承擔(dān)業(yè)務(wù)邏輯的***的救命稻草。之所以把Domain Service放在***,是因?yàn)槲姨宄I(lǐng)域服務(wù)的強(qiáng)大“魔力”了。開發(fā)人員總會有一種惰性,很多時(shí)候不愿意仔細(xì)思考所謂“職責(zé)(封裝領(lǐng)域邏輯的行為)”的正確履行者,而領(lǐng)域服務(wù)恰恰是最便捷的選擇。
就我個(gè)人的理解,只有滿足如下三個(gè)特征的領(lǐng)域行為才應(yīng)該放到領(lǐng)域服務(wù)中:
- 領(lǐng)域行為需要多個(gè)領(lǐng)域?qū)嶓w參與協(xié)作
- 領(lǐng)域行為與狀態(tài)無關(guān)
- 領(lǐng)域行為需要與外部資源(尤其是DB)協(xié)作
假設(shè)某系統(tǒng)的合同管理功能允許客戶輸入自編碼,該自編碼需要遵循一定的編碼格式。在創(chuàng)建新合同時(shí),客戶輸入自編碼,系統(tǒng)需要檢測該自編碼是否在已有合同中已經(jīng)存在。針對該需求,可以提煉出兩個(gè)領(lǐng)域行為:
- 驗(yàn)證輸入的自編碼是否符合業(yè)務(wù)規(guī)則
- 檢查自編碼是否重復(fù)
在尋找職責(zé)的履行者時(shí),我們應(yīng)首先遵循“信息專家模式”,即“擁有信息的對象就是操作該信息的專家”,因此可以提出一個(gè)問題:領(lǐng)域行為要操作的數(shù)據(jù)由誰擁有?針對***個(gè)領(lǐng)域行為,就是要確認(rèn)誰擁有自編碼格式的驗(yàn)證規(guī)則?有兩個(gè)候選:
- 擁有自編碼信息的“合同(Contract)”對象
- 體現(xiàn)自編碼知識概念自身的“自編碼(CustomizedNumber)”對象
我傾向于定義CustomizedNumber值對象,將該檢測規(guī)則封裝其內(nèi),并在構(gòu)造函數(shù)中對其進(jìn)行驗(yàn)證。在領(lǐng)域驅(qū)動設(shè)計(jì)中,值對象往往用于封裝這些基礎(chǔ)概念。由于自定義的類型可以封裝領(lǐng)域行為,就可以有效地實(shí)現(xiàn)職責(zé)的“分治”,實(shí)現(xiàn)對象的協(xié)作。
若要檢查自編碼是否重復(fù),則需要從數(shù)據(jù)庫中查找,這就需要通過Repository與DB協(xié)作?;谇懊婵偨Y(jié)的三個(gè)特征,則該職責(zé)應(yīng)該分配給一個(gè)領(lǐng)域服務(wù),例如DuplicatedNumberChecker。
從職責(zé)分配的角度看,實(shí)體Contract又或者值對象CustomizedNumber才應(yīng)該是承擔(dān)該職責(zé)的合理選擇。為何我卻定義了這么一條例外原則呢?究其原因,就是在領(lǐng)域驅(qū)動設(shè)計(jì)中,我們應(yīng)盡量保證實(shí)體與值對象的純粹性,尤其不應(yīng)該依賴于Repository(資源庫)。繼續(xù)深挖根本原因,是因?yàn)閷?shí)體與值對象的生命周期是由Repository管理的。倘若被管理的實(shí)體對象還依賴了Repository,就要求該實(shí)體對應(yīng)的Repository在管理實(shí)體對象的生命周期的同時(shí),還需要管理它與Repository的依賴,這并不合理。值對象在一個(gè)聚合(Aggregate)邊界之內(nèi),道理相同。
舉例來說,假設(shè)Contract是聚合根,如果將檢查重復(fù)編碼的職責(zé)分配給該實(shí)體對象(或值對象CustomizedNumber),內(nèi)部就需要依賴ContractRepository。然而,Contract的獲取也是通過Repository得到,在基礎(chǔ)設(shè)施層對ContractRepository的實(shí)現(xiàn)時(shí),其實(shí)并不知道該如何管理二者之間的依賴。如果Contract實(shí)體還要依賴其他Repository,就更不可能了。
- public class ContractRepositoryImpl implements ContractRepository {
- public Contract contractById(Identity contractId) {
- //這里并不知道Contract對象需要注入ContractRepository對象自身
- }
- }
若真要解決此依賴管理問題,較簡單的做法是為Contract提供一個(gè)setContractRepository()的依賴注入方法。不過,當(dāng)Contract是通過Repository來獲得時(shí),如Spring、Guice之類的DI框架都無法注入這一依賴,因而需要顯式調(diào)用,這就會引入對Repository具體實(shí)現(xiàn)的耦合。這樣的耦合放在領(lǐng)域?qū)樱瑫?dǎo)致本來單純的領(lǐng)域?qū)觾?nèi)核依賴了外部資源。倘若將這種具體耦合往外推,例如推到應(yīng)用層,又會加重調(diào)用者的負(fù)擔(dān)。
領(lǐng)域服務(wù)則不存在此問題,因?yàn)樗纳芷诓皇怯蒖epository管理。如下的領(lǐng)域服務(wù)定義是合情合理的:
- public class DuplicatedNumberChecker {
- @Repository
- private ContractRepository repository;
- public boolean isDuplicate(CustomizedNumber number) {
- return repository.existsNumber(number);
- }
- }
我們在分配領(lǐng)域邏輯時(shí),領(lǐng)域服務(wù)是最輕易也是***的***。這會導(dǎo)致領(lǐng)域服務(wù)的泛濫,長此以往,對領(lǐng)域?qū)拥拈_發(fā)又會走向“貧血模型”的老路。所謂“服務(wù)”本身就是一個(gè)抽象概念。越抽象就越顯得包容并蓄。例如定義一個(gè)OrderService,那么所有和訂單有關(guān)的邏輯都可以往這個(gè)服務(wù)里面塞,而諸如Order之類的實(shí)體對象終歸有不少限制,分配職責(zé)時(shí)需得思慮再三。因此,倘若在設(shè)計(jì)與開發(fā)時(shí)對職責(zé)的分配不加約束,所謂的“職責(zé)分治”就不過是一句空話罷了。
歸根結(jié)底,主流的領(lǐng)域驅(qū)動設(shè)計(jì)在戰(zhàn)術(shù)層面考察的其實(shí)是面向?qū)ο蟮脑O(shè)計(jì)能力。我認(rèn)為,所謂面向?qū)ο笤O(shè)計(jì),核心就是角色、職責(zé)與協(xié)作。在分配職責(zé)時(shí),應(yīng)考慮將數(shù)據(jù)與行為封裝在一起,這是面向?qū)ο笤O(shè)計(jì)的首要原則。
為了避免程序員把領(lǐng)域服務(wù)當(dāng)做一個(gè)“筐”,什么邏輯都往里面裝,除了需要提高團(tuán)隊(duì)成員面向?qū)ο蟮脑O(shè)計(jì)能力,并加強(qiáng)代碼評審之外,還有一個(gè)方法,就是對領(lǐng)域服務(wù)加以約束。
沒有任何語言可以在DDD設(shè)計(jì)要素上施加約束。Mat Wall與Nik Silver在對Guardian.co.uk網(wǎng)站推行DDD時(shí)的實(shí)踐值得我們借鑒。他們在文章《演進(jìn)架構(gòu)中的領(lǐng)域驅(qū)動設(shè)計(jì)》中建議:
為了對付這一行為,我們對應(yīng)用中的所有服務(wù)進(jìn)行了代碼評審,并進(jìn)行重構(gòu),將邏輯移到適當(dāng)?shù)念I(lǐng)域?qū)ο笾小N覀冞€制定了一個(gè)新的規(guī)則:任何服務(wù)對象在其名稱中必須包含一個(gè)動詞。這一簡單的規(guī)則阻止了開發(fā)人員去創(chuàng)建類似于ArticleService的類。取而代之,我們創(chuàng)建 ArticlePublishingService和ArticleDeletionService這樣的類。推動這一簡單的命名規(guī)范的確幫助我們將領(lǐng)域邏輯移到了正確的地方,但我們?nèi)砸髮Ψ?wù)進(jìn)行定期的代碼評審,以確保我們在正軌上,以及對領(lǐng)域的建模接近于實(shí)際的業(yè)務(wù)觀點(diǎn)。 |
其實(shí),這一別具一格的約束形式其實(shí)與服務(wù)的本質(zhì)是一脈相承的,即服務(wù)應(yīng)代表無狀態(tài)的領(lǐng)域行為,甚至可以說領(lǐng)域服務(wù)是領(lǐng)域?qū)用嬗美捏w現(xiàn)。
這一實(shí)踐可能會導(dǎo)致更多細(xì)粒度的領(lǐng)域服務(wù)產(chǎn)生,但更有可能的結(jié)果是,當(dāng)我們在創(chuàng)建一個(gè)新的領(lǐng)域服務(wù)時(shí),可能會考慮暫時(shí)停下來,想一想,要分配給這個(gè)新服務(wù)的領(lǐng)域邏輯是否有更好的去處呢?即使因?yàn)樵撨壿嬁赡軤可娴蕉鄠€(gè)領(lǐng)域?qū)嶓w,又或者需要與Repository協(xié)作而不得不放入到領(lǐng)域服務(wù)中,似乎也可以考慮將領(lǐng)域邏輯中與實(shí)體(或值對象)數(shù)據(jù)強(qiáng)相關(guān)的內(nèi)容”摘“出來,分配到合適的地方,保證職責(zé)分配的合理均衡。和諧的協(xié)作機(jī)制是好的面向?qū)ο笤O(shè)計(jì)。
【本文為51CTO專欄作者“張逸”原創(chuàng)稿件,轉(zhuǎn)載請聯(lián)系原作者】