聚合和聚合根:怎樣設計聚合?

聚合

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

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

社會由個體組成,我們每個人都是其中一員。隨着社會發展,社團、機構、部門等組織應運而生,我們從個體逐漸成爲組織的一部分。在組織中,大家協同工作,朝着共同目標奮進,能發揮出更大的力量。

在領域模型裏,實體和值對象類似於個體,而聚合則如同讓實體和值對象協同工作的組織。聚合確保這些領域對象在實現共同業務邏輯時,數據保持一致。簡單來說,聚合由業務和邏輯緊密關聯的實體和值對象組合而成,是數據修改和持久化的基本單元。每個聚合對應一個倉儲,用於實現數據持久化。

聚合有聚合根和上下文邊界。這個邊界依據業務單一職責和高內聚原則,界定了聚合內部包含的實體和值對象。而且,聚合之間的邊界是松耦合的。按此方式設計的微服務,自然具備 “高內聚、低耦合” 的特性。

在 DDD 分層架構中,聚合屬於領域層。領域層包含多個聚合,共同實現核心業務邏輯。聚合內的實體採用充血模型,實現個體業務能力以及業務邏輯的高內聚。

跨多個實體的業務邏輯通過領域服務實現,跨多個聚合的業務邏輯則通過應用服務實現。例如,若某個業務場景需要同一個聚合中的 A 和 B 兩個實體共同完成,那麼這段業務邏輯可用領域服務實現;若業務邏輯需要聚合 C 和聚合 D 中的兩個服務共同完成,這時就可以用應用服務來組合這兩個服務。

聚合根

聚合根的主要作用,是防止複雜數據模型因爲缺乏統一業務規則的管控,而出現聚合、實體之間數據不一致的情況。在傳統數據模型裏,每個實體地位平等,若任由實體隨意調用和修改數據,極有可能造成實體間數據邏輯的混亂。要是採用鎖的方式來解決,又會增加軟件複雜度,降低系統性能。

如果把聚合看作一個組織,那麼聚合根就相當於這個組織的負責人,也被稱爲根實體。它既是實體,又承擔着聚合管理者的角色。

從實體角度來看,聚合根具備實體的屬性和業務行爲,能夠實現自身的業務邏輯。

作爲聚合的管理者,聚合根在聚合內部發揮着協調作用,確保實體和值對象依照既定的業務規則,協同完成共同的業務邏輯。

在聚合之間,聚合根是聚合對外的接口。它通過聚合根 ID 關聯的方式,接收外部任務和請求,並在上下文範圍內實現聚合之間的業務協作。也就是說,聚合之間是通過聚合根 ID 進行關聯引用的。外部對象若要訪問其他聚合的實體,不能直接進行訪問,而是要先訪問聚合根,再通過聚合根導航到聚合內部的實體。

怎樣設計聚合?

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

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

在投保過程中構建聚合,可按以下步驟進行:

第一步:採用事件風暴梳理實體和值對象

基於業務行爲,運用事件風暴方法,全面梳理在投保過程中涉及這些行爲的所有實體和值對象。例如,常見的有投保單、標的、客戶、被保人等。

第二步:確定聚合根

從衆多實體中挑選出適合擔任對象管理者的根實體,即聚合根。判斷一個實體能否成爲聚合根,可結合以下場景展開分析:該實體是否擁有獨立的生命週期;是否具備全局唯一 ID;是否能夠創建或修改其他對象;是否存在專門的模塊對其進行管理。在相關圖示中,投保單和客戶實體就是聚合根。

第三步:構建聚合

依據業務單一職責和高內聚原則,找出與聚合根緊密關聯、相互依賴的所有實體和值對象。由此構建出一個對象集合,這個集合包含唯一的聚合根以及多個實體和值對象,這便是聚合。在圖中,我們構建出了客戶和投保這兩個聚合。

第四步:繪製對象引用和依賴模型

在聚合內部,根據聚合根、實體和值對象之間的依賴關係,繪製出對象的引用和依賴模型。需要特別說明的是,投保人和被保人的數據,是通過關聯客戶 ID 從客戶聚合中獲取的,在投保聚合裏,它們屬於投保單的值對象。這些值對象的數據是客戶數據的冗餘,即便未來客戶聚合的數據有所變動,也不會對投保單的值對象數據產生影響。從圖中還能清晰看到實體之間的引用關係,比如在投保聚合中,投保單聚合根引用了報價單實體,而報價單實體又引用了報價規則子實體。

第五步:劃分限界上下文

將多個聚合依據業務語義和上下文,劃分到同一個限界上下文內。

聚合的一些設計原則

我們不妨先看一下《實現領域驅動設計》一書中對聚合設計原則的描述,原文是有點不太好理解的,我來給你解釋一下。

在一致性邊界內建模真正的不變條件是 DDD 設計中的重要原則。聚合的意義在於封裝真正的不變性,並非簡單拼湊對象。每個聚合內部都有一套既定的業務規則,其中的實體和值對象遵循這些規則運轉,以此保證對象數據的一致性。而在聚合邊界之外的事物,與該聚合毫無關聯,這也正是聚合能夠達成業務高內聚的關鍵因素。

設計小聚合也十分關鍵。聚合規模若設計得過大,會因涵蓋過多實體,使實體間管理難度大增。在高頻操作場景下,易引發併發衝突或數據庫鎖問題,最終降低系統可用性。與之相對,小聚合設計能減少因業務擴張導致聚合重構的幾率,讓領域模型對業務變化的適應性更強。

聚合間的引用依靠關聯外部聚合根 ID 來實現,而非直接的對象引用。若把外部聚合的對象納入本聚合邊界內管理,不僅會模糊聚合邊界,還會提升聚合間的耦合程度,所以要通過唯一標識引用其它聚合。

聚合內部數據需保持強一致性,而聚合之間實現最終一致性即可。一次事務中,最多隻能改變一個聚合的狀態。要是業務操作涉及多個聚合狀態變更,應藉助領域事件異步修改相關聚合,從而實現聚合間的解耦(領域事件相關內容會在後續詳細講解),這就是在邊界之外使用最終一致性的原則。

爲實現微服務內聚合間的解耦,以及滿足未來以聚合爲單位的微服務組合與拆分需求,應規避跨聚合的領域服務調用和數據庫表關聯,也就是要通過應用層實現跨聚合的服務調用 。

不過,要牢記 “適合自己的纔是最好的”。在系統設計時,必須充分考量項目的實際情況。面對使用便利性、高性能需求、技術能力短板以及全局事務管理等因素時,這些原則並非不可變通,一切都應以解決實際問題爲根本出發點。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/-cGWiPv9Ad26zyG6UeI_PA