《Domain-Driven Design 領域驅動設計》Podcast 準備稿:Aggregate、Factory 與 Repository
書名: 領域驅動設計 Domain-Driven Design(Tackling Complexity in the Heart of Software) 作者: Eric Evans 系列: 啃一本大書(恩普拉氏) 公開標題建議: 啃一本大書|Domain-Driven Design.Aggregate 是 DDD 最難也最關鍵的戰術 (5/13) 涵蓋範圍: Part II 第6章 領域物件的生命週期(The Life Cycle of a Domain Object)
背景速覽
這集處理 Part II 最硬、也最實用的一章:領域物件從「建立→使用→歸檔」的整個生命週期,會碰到三個大挑戰,Evans 用三個模式分別應對——Aggregate(聚合) 定義一致性邊界、Factory(工廠) 封裝建立複雜度、Repository(儲存庫) 封裝持久化存取。其中 Aggregate 是公認 DDD 戰術設計裡最難拿捏、也最關鍵的概念,這集會花最多力氣在它身上。
一句話重點
用 Aggregate 把高度互聯的物件網絡切成有明確邊界的一致性單位(一個 Root 對外、邊界內保證 invariant),再用 Factory 把複雜的建立過程從物件職責中分離、用 Repository 把持久化機制藏起來,讓領域模型不被資料庫淹沒。
值得討論的重點
1. Aggregate:為物件網絡畫邊界
商業領域高度互聯,物件參考路徑又長又深,導致三個問題:刪除困難(共用的 Address 刪不刪?)、變更影響範圍不明、並行存取衝突(鎖太小違反 invariant、鎖太大造成爭用甚至 deadlock)。這些看似資料庫交易的技術問題,根源其實是模型缺乏邊界。
Aggregate 的定義:一群相關物件的叢集,作為資料變更的單位。每個 Aggregate 有一個 Root(唯一允許外部持有參考的 Entity)和一個 Boundary。汽車修理廠例子:Car 有全域身分(車輛識別號)是 Root,Tire 只有區域身分、包在 Car 的邊界內。
實作規則(這幾條要記熟):
- Root 有全域身分、負責檢查 invariant;邊界內 Entity 只有區域身分。
- 外部物件不能持有邊界內成員的參考(Root 除外)。
- 只有 Root 能被資料庫查詢直接取得,其他要靠 traversal。
- 刪除一次移除整個邊界內的東西。
- commit 時整個 Aggregate 的 invariant 都要滿足。
2. Purchase Order 例子:用領域知識決定邊界
採購單(PO)拆成多個 Line Item,invariant 是「所有 Line Item 總和不超過核准上限」。Evans 一步步演示鎖定粒度的兩難:鎖單一 Line Item → 兩人同時改不同 item、加總爆表沒人發現;鎖整個 PO → 保護了 invariant 但若 Part 價格也被改呢;連 Part 一起鎖 → Part 被很多 PO 共用,大量爭用甚至 deadlock。
解法是運用領域知識重新建模:分析發現 Part 變更頻率低、且價格變更不一定要立即反映到既有 PO(已歸檔的 PO 該保留當時價格)。所以把 Part 的價格複製到 Line Item,切斷即時依賴。PO + Line Item 成一個 Aggregate,Part 獨立在外。跨 Aggregate 的一致性用其他機制處理,不必是即時 invariant。這就是 Aggregate 設計的精髓:讓高爭用的關聯鬆、讓嚴格的 invariant 緊。
3. Factory:物件不該負責自己的建立
引擎的職責是轉動曲軸、不是自己組裝自己。建立複雜物件或 Aggregate 的知識,該封裝在 Factory 裡,提供反映客戶目標的介面。兩個基本要求:每個建立方法是 atomic 的、並強制所建物件的所有 invariant;Factory 抽象到所需的型別、而非具體類別。形式有三種:Aggregate Root 上的 Factory Method(擴充既有 Aggregate)、相關物件上的 Factory Method、Standalone Factory(沒有自然宿主時)。簡單情況直接用 constructor 就好,但 constructor 也要遵守 atomic + 滿足 invariant 的規則。
4. Repository:像集合一樣存取持久物件
要操作物件得先拿到參考,三種途徑:建立、走訪關聯、查詢資料庫。問題是當開發者直接面對 SQL、result set、infrastructure,模型焦點就消失了、物件退化成資料容器。Repository 把某型別的所有物件表示為一個概念上的集合——加入、移除,背後機制處理實際的資料庫操作,client 用模型的語言表達需求。
關鍵原則:只為真正需要全域存取的 Aggregate Root 提供 Repository(不是每個 class 都配一個!)。好處:解耦持久化技術、便於 in-memory dummy 測試、把交易控制權留給 client。Repository 和 Factory 分工:Factory 管「生命之初」(建立新物件),Repository 管「中段與末段」(找回既有物件),兩者可以讓 Repository 委託 Factory 做重組(reconstitution)。
注意事項
⚠️ Aggregate 是 DDD 戰術設計裡最容易做錯的。 最常見的錯:Aggregate 切太大(一個巨無霸 Root 拖著一大票物件,每次交易鎖一片、爭用嚴重)或切太小(該保護的 invariant 跨了邊界、保護不到)。Evans 給的判準是「invariant 在哪、爭用在哪」,但實務上這需要深入領域知識,沒有公式。Vaughn Vernon 後來寫了著名的《Effective Aggregate Design》三篇文章專門補這塊,可以推薦給聽眾。
⚠️ 「每個 Entity 都配一個 Repository」是常見的誤用。 Repository 只給 Aggregate Root。如果你看到 code 裡 LineItemRepository、TireRepository 這種東西,很可能 Aggregate 邊界設計有問題——邊界內的物件本來就該透過 Root 拿到,不該有自己的 Repository。
⚠️ Repository 的封裝會藏起效能陷阱。Evans 舉的真實案例:一個製造業應用上線幾小時就記憶體耗盡,因為開發者用了「all objects」查詢把整個資料庫載進記憶體,測試環境資料少所以沒發現。Client 可以忽略實作,但開發者不能——封裝不等於不用懂底層效能。
⚠️ Aggregate 邊界的決策會直接影響你後面要不要用「最終一致性」。跨 Aggregate 不保證即時一致——這正是後來微服務「每個服務一個資料庫、服務間最終一致」的理論前身。這條伏筆值得埋,第 10 集和最後一集會收。
專家補充
💡 Aggregate 是連結 DDD 戰術設計和現代分散式系統的關鍵橋樑。「一個 Aggregate = 一個一致性邊界 = 一筆交易的範圍」這個概念,在微服務時代被放大成「一個 Aggregate 往往對應一個微服務的一致性單位」。Event Sourcing、CQRS 這些架構也都繞著 Aggregate 轉(Aggregate 是事件的發源地)。所以這集表面在講戰術,其實在鋪戰略和架構的路。
💡 一個實用心法:設計 Aggregate 時,盡量讓它小。Vernon 的經驗法則是「優先用 ID 參考其他 Aggregate,而不是物件參考」——也就是 Aggregate 之間靠 ID 連、不直接持有對方物件。這能把 Aggregate 維持在小而專注的尺寸,避免巨無霸。Evans 在這章其實已經暗示了(外部不持有內部參考),Vernon 把它講得更明確。
💡 Factory 容易被「設計模式那本 GoF 的 Factory」混淆。GoF 的 Abstract Factory / Factory Method 關注的是「物件建立的多型彈性」;DDD 的 Factory 關注的是「封裝 Aggregate 建立時的 invariant 保證和複雜度」。兩者有重疊但動機不同——這正是第 9 集要講的「設計模式用在領域層要加一層領域動機」的預演。
討論問題
🎙️ 開場鉤子:「如果 DDD 戰術設計只有一個概念最難、也最值得搞懂,那就是 Aggregate。它表面在講物件分組,骨子裡在回答一個分散式系統的終極問題:一致性的邊界該畫在哪?」
🎙️ 自問自答:「Aggregate 邊界怎麼決定?」——問兩件事:哪些規則(invariant)必須在同一筆交易內保證?哪些關聯爭用很高、該鬆開?把必須一起一致的圈進來,把高爭用的推出去。
🎙️ 帶走的一題:「看看你系統裡某個『一改就要鎖一大片、常常 deadlock』的地方——很可能就是 Aggregate 邊界畫錯了,把不需要即時一致的東西綁在了一起。」
更大範圍關聯
- 戰術設計的最高峰:Aggregate/Factory/Repository 是 Part II 的壓軸,也是戰術 DDD 最常被實作的三件套。但記住——它們服務的是模型的完整性,不是為了套模式而套。
- 連到微服務 / 分散式系統:「Aggregate = 一致性邊界」是微服務「服務邊界 = 交易邊界」的理論前身。跨 Aggregate 用最終一致性,正是 CAP、Saga、事件驅動架構的思想根源。這條第 10 集會正式接上 Bounded Context。
- 連到 Event Sourcing / CQRS:這兩個現代架構都以 Aggregate 為核心(Aggregate 產生事件、CQRS 分離讀寫模型)。想深入的聽眾可往這方向延伸。
- 進階閱讀:Vaughn Vernon《Effective Aggregate Design》三部曲、《Implementing DDD》第 10 章,是 Aggregate 設計的實作聖經,補足 Evans 在落地細節上的留白。
錄製建議
- 建議時長:約 24-26 分鐘。配比:Aggregate 約 13 分(絕對的主菜,Purchase Order 例子一定要完整演一遍鎖定兩難)、Factory 約 5 分、Repository 約 6 分。
- 討論策略:Aggregate 是這集的核心,用 Purchase Order 的「鎖太小→爆表、鎖太大→爭用、複製價格切斷依賴」這條敘事線走,聽眾最有感。一直回到那把尺:「invariant 在哪、爭用在哪」。Factory 和 Repository 講重點即可(別陷入 reconstitution 的細節),重點是「Repository 只給 Root」「Factory 管出生、Repository 管找回」。
- 這集講完 Part II 的核心戰術。可以預告:下一集第7章是 Evans 把前面所有建構區塊整合進一個完整貨運系統的「實戰演練」,看模式怎麼一起運作。