DDD術語-聚合(Aggregate)、聚合根(AggregateRoot)


在事件風暴中,我們會根據一些業務操作和行為找出實體(Entity)或值對象(ValueObject),進而將業務關聯緊密的實體和值對象進行組合,構成聚合,再根據業務語義將多個聚合划定到同一個限界上下文(Bounded Context)中,並在限界上下文內完成領域建模。

那你知道為什么要在限界上下文和實體之間增加聚合和聚合根這兩個概念嗎?它們的作用是什么?怎么設計聚合?

聚合

在 DDD 中,實體和值對象是很基礎的領域對象。實體一般對應業務對象,它具有業務屬性和業務行為;而值對象主要是屬性集合,對實體的狀態和特征進行描述。但實體和值對象都只是個體化的對象,它們的行為表現出來的是個體的能力。

那聚合在其中起什么作用呢?

舉個例子。社會是由一個個的個體組成的,象征着我們每一個人。隨着社會的發展,慢慢出現了社團、機構、部門等組織,我們開始從個人變成了組織的一員,大家可以協同一致的工作,朝着一個最大的目標前進,發揮出更大的力量。

領域模型內的實體和值對象就好比個體,而能讓實體和值對象協同工作的組織就是聚合,它用來確保這些領域對象在實現共同的業務邏輯時,能保證數據的一致性。

你可以這么理解,聚合就是由業務和邏輯緊密關聯的實體和值對象組合而成的,聚合是數據修改和持久化的基本單元,每一個聚合對應一個倉儲,實現數據的持久化。

聚合有一個聚合根和上下文邊界,這個邊界根據業務單一職責和高內聚原則,定義了聚合內部應該包含哪些實體和值對象,而聚合之間的邊界是松耦合的。按照這種方式設計出來的微服務很自然就是“高內聚、低耦合”的。

聚合在 DDD 分層架構里屬於領域層,領域層包含了多個聚合,共同實現核心業務邏輯。聚合內實體以充血模型實現個體業務能力,以及業務邏輯的高內聚。跨多個實體的業務邏輯通過領域服務來實現,跨多個聚合的業務邏輯通過應用服務來實現。比如有的業務場景需要同一個聚合的 A 和 B 兩個實體來共同完成,我們就可以將這段業務邏輯用領域服務來實現;而有的業務邏輯需要聚合 C 和聚合 D 中的兩個服務共同完成,這時你就可以用應用服務來組合這兩個服務。

聚合根

聚合根的主要目的是為了避免由於復雜數據模型缺少統一的業務規則控制,而導致聚合、實體之間數據不一致性的問題。

傳統數據模型中的每一個實體都是對等的,如果任由實體進行無控制地調用和數據修改,很可能會導致實體之間數據邏輯的不一致。而如果采用鎖的方式則會增加軟件的復雜度,也會降低系統的性能。

如果把聚合比作組織,那聚合根就是這個組織的負責人。聚合根也稱為根實體,它不僅是實體,還是聚合的管理者。

首先它作為實體本身,擁有實體的屬性和業務行為,實現自身的業務邏輯。

其次它作為聚合的管理者,在聚合內部負責協調實體和值對象按照固定的業務規則協同完成共同的業務邏輯。

最后在聚合之間,它還是聚合對外的接口人,以聚合根 ID 關聯的方式接受外部任務和請求,在上下文內實現聚合之間的業務協同。也就是說,聚合之間通過聚合根 ID 關聯引用,如果需要訪問其它聚合的實體,就要先訪問聚合根,再導航到聚合內部實體,外部對象不能直接訪問聚合內實體。

怎樣設計聚合?

DDD 領域建模通常采用事件風暴,它通常采用用例分析、場景分析和用戶旅程分析等方法,通過頭腦風暴列出所有可能的業務行為和事件,然后找出產生這些行為的領域對象,並梳理領域對象之間的關系,找出聚合根,找出與聚合根業務緊密關聯的實體和值對象,再將聚合根、實體和值對象組合,構建聚合。

下面我們以保險的投保業務場景為例,看一下聚合的構建過程主要都包括哪些步驟。

 

 

 

第 1 步:采用事件風暴,根據業務行為,梳理出在投保過程中發生這些行為的所有的實體和值對象,比如投保單、標的、客戶、被保人等等。

第 2 步:從眾多實體中選出適合作為對象管理者的根實體,也就是聚合根。判斷一個實體是否是聚合根,你可以結合以下場景分析:是否有獨立的生命周期?是否有全局唯一 ID?是否可以創建或修改其它對象?是否有專門的模塊來管這個實體。圖中的聚合根分別是投保單和客戶實體。

第 3 步:根據業務單一職責和高內聚原則,找出與聚合根關聯的所有緊密依賴的實體和值對象。構建出 1 個包含聚合根(唯一)、多個實體和值對象的對象集合,這個集合就是聚合。在圖中我們構建了客戶和投保這兩個聚合。

第 4 步:在聚合內根據聚合根、實體和值對象的依賴關系,畫出對象的引用和依賴模型。這里我需要說明一下:投保人和被保人的數據,是通過關聯客戶 ID 從客戶聚合中獲取的,在投保聚合里它們是投保單的值對象,這些值對象的數據是客戶的冗余數據,即使未來客戶聚合的數據發生了變更,也不會影響投保單的值對象數據。從圖中我們還可以看出實體之間的引用關系,比如在投保聚合里投保單聚合根引用了報價單實體,報價單實體則引用了報價規則子實體。

第 5 步:多個聚合根據業務語義和上下文一起划分到同一個限界上下文內。

這就是一個聚合誕生的完整過程了。

聚合的一些設計原則

1. 在一致性邊界內建模真正的不變條件。聚合用來封裝真正的不變性,而不是簡單地將對象組合在一起。聚合內有一套不變的業務規則,各實體和值對象按照統一的業務規則運行,實現對象數據的一致性,邊界之外的任何東西都與該聚合無關,這就是聚合能實現業務高內聚的原因。

2. 設計小聚合。如果聚合設計得過大,聚合會因為包含過多的實體,導致實體之間的管理過於復雜,高頻操作時會出現並發沖突或者數據庫鎖,最終導致系統可用性變差。而小聚合設計則可以降低由於業務過大導致聚合重構的可能性,讓領域模型更能適應業務的變化。

3. 通過唯一標識引用其它聚合。聚合之間是通過關聯外部聚合根 ID 的方式引用,而不是直接對象引用的方式。外部聚合的對象放在聚合邊界內管理,容易導致聚合的邊界不清晰,也會增加聚合之間的耦合度。

4. 在邊界之外使用最終一致性。聚合內數據強一致性,而聚合之間數據最終一致性。在一次事務中,最多只能更改一個聚合的狀態。如果一次業務操作涉及多個聚合狀態的更改,應采用領域事件的方式異步修改相關的聚合,實現聚合之間的解耦。

5. 通過應用層實現跨聚合的服務調用。為實現微服務內聚合之間的解耦,以及未來以聚合為單位的微服務組合和拆分,應避免跨聚合的領域服務調用和跨聚合的數據庫表關聯。

上面的這些原則是 DDD 的一些通用的設計原則,還是那句話:“適合自己的才是最好的。”

總結

我們總結下聚合、聚合根、實體和值對象它們之間的聯系和區別。

聚合的特點:高內聚、低耦合,它是領域模型中最底層的邊界,可以作為拆分微服務的最小單位,但我不建議你對微服務過度拆分。但在對性能有極致要求的場景中,聚合可以獨立作為一個微服務,以滿足版本的高頻發布和極致的彈性伸縮能力。

一個微服務可以包含多個聚合,聚合之間的邊界是微服務內天然的邏輯邊界。有了這個邏輯邊界,在微服務架構演進時就可以以聚合為單位進行拆分和組合了,微服務的架構演進也就不再是一件難事了。

聚合根的特點:聚合根是實體,有實體的特點,具有全局唯一標識,有獨立的生命周期。一個聚合只有一個聚合根,聚合根在聚合內對實體和值對象采用直接對象引用的方式進行組織和協調,聚合根與聚合根之間通過 ID 關聯的方式實現聚合之間的協同。

實體的特點:有 ID 標識,通過 ID 判斷相等性,ID 在聚合內唯一即可。狀態可變,它依附於聚合根,其生命周期由聚合根管理。實體一般會持久化,但與數據庫持久化對象不一定是一對一的關系。實體可以引用聚合內的聚合根、實體和值對象。

值對象的特點:無 ID,不可變,無生命周期,用完即扔。值對象之間通過屬性值判斷相等性。它的核心本質是值,是一組概念完整的屬性組成的集合,用於描述實體的狀態和特征。值對象盡量只引用值對象。


免責聲明!

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



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