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