天天看點

何時定義領域服務

若遵循基于面向對象設計範式的領域驅動設計,并用以應對紛繁複雜的業務邏輯,則強調領域模型的充血設計模型已成為社群不争事實。我将Eric提及的戰術設計要素如Entity、Value Object、Domain Service、Aggregate、Repository與Factory視為設計模型。這其中,隻有Entity、Value Object和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,就更不可能了。

若真要解決此依賴管理問題,較簡單的做法是為Contract提供一個<code>setContractRepository()</code>的依賴注入方法。不過,當Contract是通過Repository來獲得時,如Spring、Guice之類的DI架構都無法注入這一依賴,因而需要顯式調用,這就會引入對Repository具體實作的耦合。這樣的耦合放在領域層,會導緻本來單純的領域層核心依賴了外部資源。倘若将這種具體耦合往外推,例如推到應用層,又會加重調用者的負擔。

領域服務則不存在此問題,因為它的生命周期不是由Repository管理。如下的領域服務定義是合情合理的:

我們在配置設定領域邏輯時,領域服務是最輕易也是最便宜的首選。這會導緻領域服務的泛濫,長此以往,對領域層的開發又會走向“貧血模型”的老路。所謂“服務”本身就是一個抽象概念。越抽象就越顯得包容并蓄。例如定義一個OrderService,那麼所有和訂單有關的邏輯都可以往這個服務裡面塞,而諸如Order之類的實體對象終歸有不少限制,配置設定職責時需得思慮再三。是以,倘若在設計與開發時對職責的配置設定不加限制,所謂的“職責分治”就不過是一句空話罷了。

歸根結底,主流的領域驅動設計在戰術層面考察的其實是面向對象的設計能力。我認為,所謂面向對象設計,核心就是角色、職責與協作。在配置設定職責時,應考慮将資料與行為封裝在一起,這是面向對象設計的首要原則。

為了避免程式員把領域服務當做一個“筐”,什麼邏輯都往裡面裝,除了需要提高團隊成員面向對象的設計能力,并加強代碼評審之外,還有一個方法,就是對領域服務加以限制。

沒有任何語言可以在DDD設計要素上施加限制。Mat Wall與Nik Silver在對Guardian.co.uk網站推行DDD時的實踐值得我們借鑒。他們在文章《演進架構中的領域驅動設計》中建議:

為了對付這一行為,我們對應用中的所有服務進行了代碼評審,并進行重構,将邏輯移到适當的領域對象中。我們還制定了一個新的規則:任何服務對象在其名稱中必須包含一個動詞。這一簡單的規則阻止了開發人員去建立類似于ArticleService的類。取而代之,我們建立 ArticlePublishingService和ArticleDeletionService這樣的類。推動這一簡單的命名規範的确幫助我們将領域邏輯移到了正确的地方,但我們仍要求對服務進行定期的代碼評審,以確定我們在正軌上,以及對領域的模組化接近于實際的業務觀點。

其實,這一别具一格的限制形式其實與服務的本質是一脈相承的,即服務應代表無狀态的領域行為,甚至可以說領域服務是領域層面用例的展現。

這一實踐可能會導緻更多細粒度的領域服務産生,但更有可能的結果是,當我們在建立一個新的領域服務時,可能會考慮暫時停下來,想一想,要配置設定給這個新服務的領域邏輯是否有更好的去處呢?即使因為該邏輯可能牽涉到多個領域實體,又或者需要與Repository協作而不得不放入到領域服務中,似乎也可以考慮将領域邏輯中與實體(或值對象)資料強相關的内容”摘“出來,配置設定到合适的地方,保證職責配置設定的合理均衡。和諧的協作機制是好的面向對象設計。