DDD 領域驅動設計學習(五)- 實體/值對象/領域服務


領域驅動設計DDD在戰術建模上提供了一個元模型體系(如下圖):
 
DDD構建的元模型元素腦圖

元模型往往用來在某一特定的領域定義一個基礎的通用的語言,來討論和描述該領域的問題及解決方法。可以將元模型想象成為某種形式語言,這樣模型就是一篇用該語言描述的文章,其中元模型中的元素就是該語言的詞匯,元素之間的關系就是該語言的語法。元模型的例子其實很多,例如交通指示標志就定義了一種非常簡單的交通規則的元模型。DDD的元模型圖也是用於描述如何去創建一個DDD的模型。

DDD的戰術階段實際就是這樣一個抽象過程。這個抽象過程由於元模型的存在實際是一定程度模式化的。這樣的好處是並非只能技術人員參與建模,業務人員經過一定的培訓也是完全可以理解的。

DDD的戰術建模包括如下內容:

  1. 實體-Entity
  2. 值對象-Value Objects
  3. 領域服務-Domain Services
  4. 領域事件-Domain Events
  5. 模塊-Modules
  6. 聚合-Aggregate
  7. 資源庫-Repository

實體和值對象

實體和值對象放在一起講容易區分,概括而言,實體不僅需要知道它是什么?而且還需要知道它是哪個?而值對象只需要知道它是什么?先看定義:

定義:

  • 實體:許多對象不是由它們的屬性來定義,而是通過一系列的連續性(continuity)和標識(identity)來從根本上定義的。只要一個對象在生命周期中能夠保持連續性,並且獨立於它的屬性(即使這些屬性對系統用戶非常重要),那它就是一個實體。
  • 值對象:當你只關心某個對象的屬性時,該對象便可作為一個值對象。為其添加有意義的屬性,並賦予它相應的行為。我們需要將值對象看成不變對象,不要給它任何身份標識,還應該盡量避免像實體對象一樣的復雜性。

對於實體Entity,實體核心是用唯一的標識符來定義,而不是通過屬性來定義。即即使屬性完全相同也可能是兩個不同的對象。同時實體本身有狀態的,實體又演進的生命周期,實體本身會體現出相關的業務行為,業務行為會實體屬性或狀態造成影響和改變。

如果從值對象本身無狀態,不可變,並且不分配具體的標識層面來看。那么值對象可以僅僅理解為實際的Entity對象的一個屬性結合而已。該值對象附屬在一個實際的實體對象上面。值對象本身不存在一個獨立的生命周期,也一般不會產生獨立的行為。

初看還是很難理解,舉幾個例子:

案例分析

  1. 營業廳會賣手機以及很多手機配件,在客戶業務規則中,往往每一部手機都要單獨管理,通過手機的SN號來識別。而手機配件是一種數量類型的實物,只關心其數量的變化,並不關心到每一個具體的手機配件。這種場景就是典型的實體和對象的案例。
  2. 地址是實體還是值對象。在電力公司服務軟件中,一個地址對應於公司線路和服務的目的地。如果多個住所都申請了電力服務,那么這個公司需要知道這一點,因此地址是實體。我們也可以用另一種方法,在模型中將“住所”關聯到運營服務,其中“住所”是一個包含地址屬性的實體。此時,地址就是一個值對象。
  3. 體育場座位例子。當我們發放的門票上有座位號的時候,座位需要作為獨立的實體,座位號是唯一的標識。而當先到先座模式下,我們只關心剩余座位數,那么座位號並不是唯一標識,這時候座位就可以作為一個值對象。這跟我們的業務需求有關。
  4. 消息場景中,發件人、收件人是實體?還是值對象?這個在 三個問題思考實體和值對象一文中有討論。
  5. 值對象的常見例子包括數字,比如100和293.51;或者文本字符串,比如"hello world";或者日期時間;還有更加詳細的對象,此如某人的全名,其中包含姓改、名字和頭銜;再比如貨幣、顏色、電話號碼和郵寄地址等。當然還有更加復雜的值對象。這種對象無狀態,本身不產生行為,不存在生命周期演進。

值對象的目的和使用

實體對象相對容易理解,我們常見的類的都可以看成是實體對象。值對象在DDD中相對而言是難以理解並且容易誤用的。

為什么需要使用值對象,書中給了一個解釋:

使用不變的值對象使得我們做更少的職責假設

個人理解這個還是基於BC的封閉性而言的,使用值對象在不同的BC中進行數據交換,可以避免不同BC對實體對象的狀態變更而引發的數據依賴關系,實現最小化的集成。另外可以從目前流行的Stateless Service角度考慮值對象的價值。

開發者因為習慣趨向於將關注點放在數據而不是領域上。在軟件開發中,數據庫依然占據着主導地位。我們首先考慮的是數據的屬性(對應數據庫的列)和關聯關系(外鍵關聯),而不是富有行為的領域概念。這樣做的結果是將數據模型直接反映在對象模型上,導致產生貧血型的領域模型的實體。雖然在實體模型中加入getter和setter並不是什么大錯,但這卻不是DDD的做法。

值類型用於度量和描述事物,DDD中建議應盡量使用值對象來建模而不是實體對象,因為值對象非常容易地對值對象進行創建、測試、使用、優化和維護。

關於值對象,它擁有以下一些特征:

  1. 它度量或者描述了領城中的一件東西。
  2. 它可以作為不變量。
  3. 它將不同的相關的屬性組合成一個概念整體(Conceptual Whole)
  4. 當度量和描述改變時,可以用另一個值對象予以替換。
  5. 它可以和其他值對象進行相等性比較。
  6. 它不會對協作對象造成副作用

一個對象的方法可以設計成一個無副作用函數(Side-Effect-Free Function) 。這里的函數表示對某個對象的操作,它只用於產生輸出, 而不會修改對象的狀態。由於在函數執行的過程中沒有狀態改變,這樣的函數操作也稱為無副作用函數。對於不變的值對象而言,所有的方法都必須是無副作用函數,因為它們不能破壞值對象的不變性。

最小化集成

在所有的DDD項目中,通常存在多個限界上下文,這意味着我們需要找到合適的方法對這些上下文進行集成。當模型概念從上游上下文流入下游上下文中時, 盡量使用值對象來表示這些概念。這樣的好處是可以達到最小化集成,即可以最小化下游模型中用於管理職責的屬性數目。使用不變的值對象使得我們做更少的職責假設。

領域服務

領域中的服務表示一個無狀態的操作,它用於實現特定於某個領域的任務。
當某個揉作不適合放在聚合和值對象上時,最好的方式便是使用領域服務了。有時我們傾向於使用聚合根上的靜態方法來實現這些這些操作,但是在 DDD中,這是一種壞味道。

什么是領域服務(首先,什么不是領域服務)

聽到"服務"這個詞時,我們自然地可能會想到一個分布式系統的遠程調用場景。可能是一個SOA的服務,也有多種技術和方法可以實現SOA服務,例如遠程過程調用(RPC)或者面向消息的中間件(MoM)。

但這些都不是領域服務。

另外也不要將領域服務與應用服務混雜在一起了。在應用服務中,我們並不會處理業務邏輯,但是領域服務卻拾恰是處理業務邏輯的。簡單來講,應用服務是領域模型很自然的客戶方,進而也是領域服務的客戶方。

雖然領域服務中有"服務"這個同,但它並不意昧着需要遠程的、重量級的事務操作。

領域模型中的服務是一種非常好的建模工具,現在我們已經知道領域服務不是什么了,那么它到底義是什么昵?

有時,它不見得是一件東西…...當領域中的某個操作過程或轉換過程不是實體或值對象的職責時,此時我們便成該將該操作放在一個單獨的接口中,即領域服務。請確保該領域服務和通用語言是一致的;並且保證它是無狀態的。[Evans, pp. 104,106]

那么在什么情況下,某個操作不屬於實體或者值對象呢?書中羅列了以下幾點:

  • 執行一個顯著的業務操作過程。
  • 對領域對象進行轉換。
  • 以多個領域對象作為輸入進行計算,結果產生一個值對象。

對於最后一點中的計算過程,它應該具有“顯著的業務操作過程"的特點。這也是領域服務很常見的應用場景,它可能需要多個聚合作為輸人。 當一個方法不便放在實體或值對象上時,使用領域服務便是最佳的解決方法。需要確保領域服務是無狀態的,並且能夠明確地表達限界上下文中的通用語言 。

不過只要在真正必要是才應該使用領域服務,過度使用領域服務將會導致貧血領域模型,所有業務都位於領域服務中,而不是實體和值對象中了。

用戶權限認證的例子

《實現領域驅動設計》書中給出了一個例子,對User進行認證的例子。例子中給出的需求是:

  • 系統必須對User進行認證,並且只有當Tenant處於激活狀態時候才能對User進行認證。
  • 必須對密碼進行加密,並且不能使用明文密碼
    對以上的需求,我們可以把認證的方法寫在User類或者Tenant類中,不過對於以上解決方案,似乎都給模型帶來了太多的問題。對於后一種方案, 我們必須從以下回種解決辦法中選擇一種:
  1. 在Tenant中處理對密碼的加密,然后將加密后的密碼傳給User。這種方法違背了單一職責原則
  2. 由於一個User必須保征對密碼的加密,它可能已經知道了一些加密信息。如果是這樣,我們可以在User上創建一個方法,該方法對明文密碼進行認證。但是在這種方式下,認證過程變成了Tenant上的Facade。而實際的認證 功能全在User上。另外User上的認證方法必須聲明為Protected,以防止外界 客戶端對認證方法的直接調用。
  3. Tenant依賴於User對密碼進行加密,然后將加密后的密碼與原有密碼進行匹配。這種方法似乎在對象協作之間增加了額外的步驟。此時,Tenant依然需 要知道認證細節。
  4. 讓客戶端對密碼進行加密。然后將其傳給Tenant,這樣導致的問題在於客戶端承載了它本不應該有的職責。

以上這些方法都有問題,這時候選擇通過領域服務會是一個簡單而優雅的選擇。

UserDescriptor userDescriptor = 
          DomainRegistry
            .authenticationService()
            .authenticate(tenantID,userName,password);

為領域服務創建一個迷你層

一個方法是放入領域對象還是放入領域服務有時候會是一個比較困難的選擇。我們可能希望在實體和值對象之上創建一個領域服務的迷你層,這樣簡化了分析的工作,但這樣做可能會導致貧血領域模型這種反模式。
對於有些系統來說,為領域服務創建一個不至於導致貧血領域模型的迷你層還是值得的。當然這取決於領城模型的特征。對於上面提到的身份與訪問上下文來說,這樣的做法是非常有用的。
如果你決定為領域服務創建一個迷你層,需要注意這樣的迷你層和應用層中的服務是不同的。在應用服務中,我們關心的是事務和安全,但是這些不應該出現在領域服務中。

領域事件

參考 DDD 領域驅動設計學習筆記(三)- 領域事件

模塊

模塊在技術上可以對應Java中的Package。在DDD中,模塊表示了一個命名的容器,用於存放領域中內聚在一起的類。

模塊應該包含一組具有高內聚性的概念集合.這樣做的好處是可以在不同的模塊之間實現松耦合。否則,我們應該修改模型以重新划分這些概念。……由於模塊名是UL的一部分,模塊名應該反映出它們在領域中的概念。[Evans]

 

 

模塊的設計是基於領域模型的,要符合通用語言的表述。其次,模塊的設計要符合高內聚低耦合的設計思想。設計模塊時候,有幾條簡單原則如下:
 
設計模塊的簡單原則

模塊和BC的關系

模塊與子域和限界上下文並不是一致的概念,模塊也是一種獨立的建模方法。對於何時應該對領域模型進行分離,何時將領域模型建模成一個整體,應該仔細地思考與對待。有時通用語言可以很好地幫助我們做出正確的選擇。但是另外的時候,其中的術語將變得非常含糊。在這種情況下,我們並不清楚如何划分上下文邊界。此時,我們可以首先將它們放在一起,使用模塊來對模型進行划分,面不是限界上下文。

但是,這並不意味着我們就應該限制對限界上下文的創建。我們應該通過通用語言的需求來划分模型邊界。但限界上下文不是用來代替模塊的。使用摸塊的目的在於組織那些內聚在一起的領域對象,對於那些內聚性不強或者沒有內聚性的領域對象來說,我們應該將它們划分在不同的模塊中。

 



作者:數行者
鏈接:https://www.jianshu.com/p/da51d16dbdc4
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM