聚合與聚合根的含義
聚合: 聚合往往是一些實體為了某項業務而聚類在一起形成的集合 , 舉個例子, 社會是由一個個的個體組成的,象徵著我們每一個人。隨著社會的發展,慢慢出現了社團、機構、部門等組織,我們開始從個人變成了組織的一員,大家可以協同一致的工作,朝著一個最大的目標前進,發揮出更大的力量。領域模型內的實體和值物件就好比個體,而能讓實體和值物件協同工作的組織就是聚合,它用來確保這些領域物件在實現共同的業務邏輯時,能保證資料的一致性。可以這麼理解,聚合就是由業務和邏輯緊密關聯的實體和值物件組合而成的,聚合是資料修改和持久化的基本單元,每一個聚合對應一個倉儲,實現資料的持久化。
聚合根 : 前面有提到, 聚合有一個聚合根和上下文邊界,這個邊界根據業務單一職責和高內聚原則,定義了聚合內部應該包含哪些實體和值物件,而聚合之間的邊界是鬆耦合的。按照這種方式設計出來的微
服務很自然就是“高內聚、低耦合”的。聚合根的主要目的是為了避免由於複雜資料模型缺少統一的業務規則控制,而導致聚合、實體之間資料不一致性的問題。傳統資料模型中的每一個實體都是對等的,如果任由實體進行無控制地呼叫和資料修改,很可能會導致實體之間資料邏輯的不一致。而如果採用鎖的方式則會增加軟體的複雜度,也會降低系統的效能。
如果把聚合比作組織,那聚合根就是這個組織的負責人。聚合根也稱為根實體,它不僅是實體,還是聚合的管理者。首先它作為實體本身,擁有實體的屬性和業務行為,實現自身的業務邏輯。其次它作為聚合的管理者,在聚合內部負責協調實體和值物件按照固定的業務規則協同完成共同的業務邏輯。
最後在聚合之間,它還是聚合對外的介面人,以聚合根 ID 關聯的方式接受外部任務和請求,在上下文內實現聚合之間的業務協同。也就是說,聚合之間通過聚合根 ID 關聯引用,如果需要訪問其它聚合的實體,就要先訪問聚合根,再導航到聚合內部實體,外部物件不能直接訪問聚合內實體。
怎樣設計聚合?
DDD 領域建模通常採用事件風暴,它通常採用用例分析、場景分析和使用者旅程分析等方法,通過頭腦風暴列出所有可能的業務行為和事件,然後找出產生這些行為的領域物件,並梳理領域物件之間的關係,找出聚合根,找出與聚合根業務緊密關聯的實體和值物件,再將聚合根、實體和值物件組合,構建聚合。
下面我們以保險的投保業務場景為例,看一下聚合的構建過程主要都包括哪些步驟。
第 1 步:採用事件風暴,根據業務行為,梳理出在投保過程中發生這些行為的所有的實體和值物件,比如投保單、標的、客戶、被保人等等。
第 2 步:從眾多實體中選出適合作為物件管理者的根實體,也就是聚合根。判斷一個實體是否是聚合根,你可以結合以下場景分析:是否有獨立的生命週期?是否有全域性唯一 ID?是否可以建立或修改其它物件?是否有專門的模組來管這個實體。圖中的聚合根分別是投保單和客戶實體。
第 3 步:根據業務單一職責和高內聚原則,找出與聚合根關聯的所有緊密依賴的實體和值物件。構建出 1 個包含聚合根(唯一)、多個實體和值物件的物件集合,這個集合就是聚合。在圖中我們構建了客戶和投保這兩個聚合。
第 4 步:在聚合內根據聚合根、實體和值物件的依賴關係,畫出物件的引用和依賴模型。這裡我需要說明一下:投保人和被保人的資料,是通過關聯客戶 ID 從客戶聚合中獲取的,在投保聚合裡它們是投保單的值物件,這些值物件的資料是客戶的冗餘資料,即使未來客戶聚合的資料發生了變更,也不會影響投保單的值物件資料。從圖中我們還可以看出實體之間的引用關係,比如在投保聚合裡投保單聚合根引用了報價單實體,報價單實體則引用了報價規則子實體。
第 5 步:多個聚合根據業務語義和上下文一起劃分到同一個限界上下文內。這就是一個聚合誕生的完整過程了。
聚合的一些設計原則我們不妨先看一下《實現領域驅動設計》一書中對聚合設計原則的描述,原文是有點不太好理解的,我來給你解釋一下。
-
在一致性邊界內建模真正的不變條件。聚合用來封裝真正的不變性,而不是簡單地將物件組合在一起。聚合內有一套不變的業務規則,各實體和值物件按照統一的業務規則執行,實現物件資料的一致性,邊界之外的任何東西都與該聚合無關,這就是聚合能實現業務高內聚的原因。
-
設計小聚合。如果聚合設計得過大,聚合會因為包含過多的實體,導致實體之間的管理過於複雜,高頻操作時會出現併發衝突或者資料庫鎖,最終導致系統可用性變差。而小聚合設計則可以降低由於業務過大導致聚合重構的可能性,讓領域模型更能適應業務的變化。
-
通過唯一標識引用其它聚合。聚合之間是通過關聯外部聚合根 ID 的方式引用,而不是直接物件引用的方式。外部聚合的物件放在聚合邊界內管理,容易導致聚合的邊界不清晰,也會增加聚合之間的耦合度。
-
在邊界之外使用最終一致性。聚合內資料強一致性,而聚合之間資料最終一致性。在一次事務中,最多隻能更改一個聚合的狀態。如果一次業務操作涉及多個聚合狀態的更改,應採用領域事件的方式非同步修改相關的聚合,實現聚合之間的解耦(相關內容我會在領域事件部分詳解)。
-
通過應用層實現跨聚合的服務呼叫。為實現微服務內聚合之間的解耦,以及未來以聚合為單位的微服務組合和拆分,應避免跨聚合的領域服務呼叫和跨聚合的資料庫表關聯。上面的這些原則是 DDD 的一些通用的設計原則,還是那句話:“適合自己的才是最好的。”在系統設計過程時,你一定要考慮專案的具體情況,如果面臨使用的便利性、高效能要求、技術能力缺失和全域性事務管理等影響因素,這些原則也並不是不能突破的,總之一切以解決實際問題為出發點。
聚合、聚合根、實體和值物件的聯絡和區別
聚合的特點:高內聚、低耦合,它是領域模型中最底層的邊界,可以作為拆分微服務的最小單位,但我不建議你對微服務過度拆分。但在對效能有極致要求的場景中,聚合可以獨立作為一個微服務,以滿足版本的高頻釋出和極致的彈性伸縮能力。
一個微服務可以包含多個聚合,聚合之間的邊界是微服務內天然的邏輯邊界。有了這個邏輯邊界,在微服務架構演進時就可以以聚合為單位進行拆分和組合了,微服務的架構演進也就不再是一件難事了。
聚合根的特點:聚合根是實體,有實體的特點,具有全域性唯一標識,有獨立的生命週期。一個聚合只有一個聚合根,聚合根在聚合內對實體和值物件採用直接物件引用的方式進行組織和協調,聚合根與聚合根之間通過 ID 關聯的方式實現聚合之間的協同。
實體的特點:有 ID 標識,通過 ID 判斷相等性,ID 在聚合內唯一即可。狀態可變,它依附於聚合根,其生命週期由聚合根管理。實體一般會持久化,但與資料庫持久化物件不一定是一對一的關係。實體可以引用聚合內的聚合根、實體和值物件。
值物件的特點:無 ID,不可變,無生命週期,用完即扔。值物件之間通過屬性值判斷相等
性。它的核心本質是值,是一組概念完整的屬性組成的集合,用於描述實體的狀態和特徵。
值物件儘量只引用值物件。
站在巨人的肩膀上
- 極客時間歐創新DDD實踐課
- 田園裡的蟋蟀我的領域驅動設計之路
- dax.net領域驅動設計實踐