聚合和聚合根:怎樣設計聚合?你知道嗎?
聚合
在 DDD 中,實體和值對象是很基礎的領域對象。實體一般對應業務對象,它具有業務屬性和業務行為;而值對象主要是屬性集合,對實體的狀態和特征進行描述。但實體和值對象都只是個體化的對象,它們的行為表現出來的是個體的能力。
那聚合在其中起什么作用呢?
社會由個體組成,我們每個人都是其中一員。隨著社會發展,社團、機構、部門等組織應運而生,我們從個體逐漸成為組織的一部分。在組織中,大家協同工作,朝著共同目標奮進,能發揮出更大的力量。
在領域模型里,實體和值對象類似于個體,而聚合則如同讓實體和值對象協同工作的組織。聚合確保這些領域對象在實現共同業務邏輯時,數據保持一致。簡單來說,聚合由業務和邏輯緊密關聯的實體和值對象組合而成,是數據修改和持久化的基本單元。每個聚合對應一個倉儲,用于實現數據持久化。
聚合有聚合根和上下文邊界。這個邊界依據業務單一職責和高內聚原則,界定了聚合內部包含的實體和值對象。而且,聚合之間的邊界是松耦合的。按此方式設計的微服務,自然具備“高內聚、低耦合”的特性。
在DDD分層架構中,聚合屬于領域層。領域層包含多個聚合,共同實現核心業務邏輯。聚合內的實體采用充血模型,實現個體業務能力以及業務邏輯的高內聚。
跨多個實體的業務邏輯通過領域服務實現,跨多個聚合的業務邏輯則通過應用服務實現。例如,若某個業務場景需要同一個聚合中的A和B兩個實體共同完成,那么這段業務邏輯可用領域服務實現;若業務邏輯需要聚合C和聚合D中的兩個服務共同完成,這時就可以用應用服務來組合這兩個服務。
聚合根
聚合根的主要作用,是防止復雜數據模型因為缺乏統一業務規則的管控,而出現聚合、實體之間數據不一致的情況。在傳統數據模型里,每個實體地位平等,若任由實體隨意調用和修改數據,極有可能造成實體間數據邏輯的混亂。要是采用鎖的方式來解決,又會增加軟件復雜度,降低系統性能。
如果把聚合看作一個組織,那么聚合根就相當于這個組織的負責人,也被稱為根實體。它既是實體,又承擔著聚合管理者的角色。
從實體角度來看,聚合根具備實體的屬性和業務行為,能夠實現自身的業務邏輯。
作為聚合的管理者,聚合根在聚合內部發揮著協調作用,確保實體和值對象依照既定的業務規則,協同完成共同的業務邏輯。
在聚合之間,聚合根是聚合對外的接口。它通過聚合根 ID 關聯的方式,接收外部任務和請求,并在上下文范圍內實現聚合之間的業務協作。也就是說,聚合之間是通過聚合根 ID 進行關聯引用的。外部對象若要訪問其他聚合的實體,不能直接進行訪問,而是要先訪問聚合根,再通過聚合根導航到聚合內部的實體。
怎樣設計聚合?
DDD 領域建模通常采用事件風暴,它通常采用用例分析、場景分析和用戶旅程分析等方法,通過頭腦風暴列出所有可能的業務行為和事件,然后找出產生這些行為的領域對象,并梳理領域對象之間的關系,找出聚合根,找出與聚合根業務緊密關聯的實體和值對象,再將聚合根、實體和值對象組合,構建聚合。
下面我們以保險的投保業務場景為例,看一下聚合的構建過程主要都包括哪些步驟。
圖片
在投保過程中構建聚合,可按以下步驟進行:
第一步:采用事件風暴梳理實體和值對象
基于業務行為,運用事件風暴方法,全面梳理在投保過程中涉及這些行為的所有實體和值對象。例如,常見的有投保單、標的、客戶、被保人等。
第二步:確定聚合根
從眾多實體中挑選出適合擔任對象管理者的根實體,即聚合根。判斷一個實體能否成為聚合根,可結合以下場景展開分析:該實體是否擁有獨立的生命周期;是否具備全局唯一 ID;是否能夠創建或修改其他對象;是否存在專門的模塊對其進行管理。在相關圖示中,投保單和客戶實體就是聚合根。
第三步:構建聚合
依據業務單一職責和高內聚原則,找出與聚合根緊密關聯、相互依賴的所有實體和值對象。由此構建出一個對象集合,這個集合包含唯一的聚合根以及多個實體和值對象,這便是聚合。在圖中,我們構建出了客戶和投保這兩個聚合。
第四步:繪制對象引用和依賴模型
在聚合內部,根據聚合根、實體和值對象之間的依賴關系,繪制出對象的引用和依賴模型。需要特別說明的是,投保人和被保人的數據,是通過關聯客戶 ID 從客戶聚合中獲取的,在投保聚合里,它們屬于投保單的值對象。這些值對象的數據是客戶數據的冗余,即便未來客戶聚合的數據有所變動,也不會對投保單的值對象數據產生影響。從圖中還能清晰看到實體之間的引用關系,比如在投保聚合中,投保單聚合根引用了報價單實體,而報價單實體又引用了報價規則子實體。
第五步:劃分限界上下文
將多個聚合依據業務語義和上下文,劃分到同一個限界上下文內。
聚合的一些設計原則
我們不妨先看一下《實現領域驅動設計》一書中對聚合設計原則的描述,原文是有點不太好理解的,我來給你解釋一下。
在一致性邊界內建模真正的不變條件是 DDD 設計中的重要原則。聚合的意義在于封裝真正的不變性,并非簡單拼湊對象。每個聚合內部都有一套既定的業務規則,其中的實體和值對象遵循這些規則運轉,以此保證對象數據的一致性。而在聚合邊界之外的事物,與該聚合毫無關聯,這也正是聚合能夠達成業務高內聚的關鍵因素。
設計小聚合也十分關鍵。聚合規模若設計得過大,會因涵蓋過多實體,使實體間管理難度大增。在高頻操作場景下,易引發并發沖突或數據庫鎖問題,最終降低系統可用性。與之相對,小聚合設計能減少因業務擴張導致聚合重構的幾率,讓領域模型對業務變化的適應性更強。
聚合間的引用依靠關聯外部聚合根 ID 來實現,而非直接的對象引用。若把外部聚合的對象納入本聚合邊界內管理,不僅會模糊聚合邊界,還會提升聚合間的耦合程度,所以要通過唯一標識引用其它聚合。
聚合內部數據需保持強一致性,而聚合之間實現最終一致性即可。一次事務中,最多只能改變一個聚合的狀態。要是業務操作涉及多個聚合狀態變更,應借助領域事件異步修改相關聚合,從而實現聚合間的解耦(領域事件相關內容會在后續詳細講解),這就是在邊界之外使用最終一致性的原則。
為實現微服務內聚合間的解耦,以及滿足未來以聚合為單位的微服務組合與拆分需求,應規避跨聚合的領域服務調用和數據庫表關聯,也就是要通過應用層實現跨聚合的服務調用 。
不過,要牢記 “適合自己的才是最好的”。在系統設計時,必須充分考量項目的實際情況。面對使用便利性、高性能需求、技術能力短板以及全局事務管理等因素時,這些原則并非不可變通,一切都應以解決實際問題為根本出發點。