第 6 / 13 部 待錄

貨運系統實戰演練

《Domain-Driven Design 領域驅動設計》Podcast 準備稿:貨運系統實戰演練

書名: 領域驅動設計 Domain-Driven Design(Tackling Complexity in the Heart of Software) 作者: Eric Evans 系列: 啃一本大書(恩普拉氏) 公開標題建議: 啃一本大書|Domain-Driven Design.把所有戰術模式串起來實戰 (6/13) 涵蓋範圍: Part II 第7章 使用語言:一個延伸範例(Using the Language: An Extended Example)

背景速覽

Part II 的收尾,也是全書唯一一章「完整實戰演練」。Evans 拿一個貨運系統(Cargo Shipping System),把前面所有建構區塊——Entity、Value Object、Aggregate、Repository、Factory、Module、Service——全部串起來用一遍。這章的價值不在新概念,而在示範這些模式如何協同運作、以及設計如何透過重構持續演化。最後還預告了一個戰略概念:Anticorruption Layer(防腐層)。這集偏「看大師怎麼做」,輕鬆但很有啟發。

一句話重點

戰術模式不是各自獨立的工具,而是一套協同的語言——這章用貨運系統完整示範:從初始模型,逐步施加 Entity/VO 分類、遍歷方向、Aggregate 邊界、Repository 選擇,再到整合外部系統,每一步都用統一語言做決策、用重構修正設計。

值得討論的重點

1. 從初始模型到逐步施加約束

Evans 從一張貨運領域的類別圖出發,核心概念有:Cargo(貨物)、Customer(不同 Role)、Delivery Specification(交付目標)、Carrier Movement(一趟運輸)、Handling Event(裝卸事件)、Delivery History(實際發生的事)。然後一步步「收緊」設計:

  • 分 Entity / Value Object:Cargo、Customer、Handling Event、Location 是 Entity(要追蹤身分);Delivery Specification、Role、時間戳是 Value Object。特殊案例 Delivery History 是 Entity,但它的身分是從擁有它的 Cargo 借來的——這個細節為 Aggregate 邊界鋪路。
  • 設計遍歷方向:取消 Customer → Cargo 的直接引用(老客戶會累積大量 Cargo、讓 Customer 變笨重),要查就用資料庫查詢。每個方向選擇都在捕捉領域洞見。

2. Aggregate 邊界與 Repository 選擇

Customer、Location、Carrier Movement 各自被多個 Cargo 共用,必須是各自 Aggregate 的 Root。Cargo 也是 Root,但邊界要仔細想:Delivery History 沒人會脫離 Cargo 單獨查、身分又是借來的 → 放進 Cargo 的邊界內;Handling Event 雖因 Cargo 而存在,但「查某趟 Carrier Movement 上所有裝卸」這種需求讓它該是自己 Aggregate 的 Root

Repository 只給五個 Aggregate Root,而且要看應用需求才設:Customer/Location/Carrier Movement/Cargo Repository 都有明確的應用觸發,但暫時沒有 Handling Event Repository(第一版用集合實作、還沒有對應需求)。這示範了「Repository 不是自動配給每個 Root,是按需求設」。

3. 場景走查:用情境驗證設計

Evans 走過幾個場景交叉驗證:

  • 變更目的地:Delivery Specification 是 Value Object → 直接丟舊的、建新的、替換。
  • 重複業務(Repeat Business):用舊 Cargo 當新 Cargo 的原型(Prototype 模式),但要仔細處理 Aggregate 邊界內每個物件的複製策略——Delivery History 建新空的、Customer Roles 複製 Map 但不複製 Customer 物件本身(它在邊界外)、Tracking ID 取新的。重點:複製只動邊界內、完全不影響邊界外。

4. 重構的暫停:用查詢取代集合

很經典的一段。原本 Delivery History 用一個 List 直接持有 Handling Event 集合,但發現新增 Handling Event 會把 Cargo Aggregate 捲入交易——而「記錄裝卸事件」本該是快速簡單的作業,不該有爭用。解法:用查詢(query)取代集合,新增 Handling Event Repository。好處:Handling Event 新增不再引發 Cargo 邊界外的完整性問題、交易能無干擾完成、循環引用的維護也簡化。這示範了 Evans 反覆強調的:正確建模 Value/Entity/Aggregate,能降低設計變更的影響範圍。

注意事項

⚠️ 這章是「看示範」,別逼自己背下貨運系統的每個物件。重點是抓決策的思路——每一步 Evans 都在問「這要不要追蹤身分」「這個遍歷方向反映什麼領域偏好」「這個 Aggregate 邊界保護什麼 invariant」。把這套提問方式帶走,比記住 Cargo 有幾個關聯有用一百倍。

⚠️ Module 劃分這段有個血淋淋的反例:按「模式」分組(所有 Entity 一包、所有 VO 一包)。結果是低內聚(概念無關的物件被塞一起)、高耦合(關聯在所有 Module 間亂竄)。Evans 的話很重:這些套件講的是「開發者當時在讀什麼書」,不是「貨運領域的故事」。正解是按領域概念分(Customer / Shipping / Billing),Module 名要進統一語言。

⚠️ 章末出現了 Anticorruption Layer(防腐層)Enterprise Segment——整合外部 Sales Management System 時,Evans 不讓 Booking 直接遷就對方系統的設計,而是建一個 Allocation Checker 在兩個模型之間翻譯。這是戰略設計(第 10 集)的預告,這集點到為止、埋伏筆即可。

專家補充

💡 這個「貨運系統」是 DDD 社群的傳奇案例。Evans 用它貫穿全書(前面幾章的 Routing Service、Itinerary 都是它),後來甚至有開源專案 DDDSample(Citerus 公司做的)把這個貨運系統完整實作出來,成為學 DDD 的標準參考程式碼。聽眾如果想看「DDD 到底長怎樣的 code」,去 GitHub 搜 dddsample 是最好的起點。

💡 「用查詢取代集合」這個重構,背後其實是一個現代架構常見的張力:物件模型想用「Delivery History 持有 Event 集合」這種乾淨的物件關係,但效能和交易邊界逼你改用查詢。這正是後來 CQRS(命令查詢職責分離)要系統化解決的問題——寫入用 Aggregate、讀取用專門的查詢模型。Evans 在 2003 就摸到了這個邊界。

💡 這章最值得學的是 Evans 的「設計節奏」:不是一次想到完美設計,而是先做一個合理版本 → 用場景走查 → 發現問題 → 重構。他甚至在章節中段「暫停下來重構」。這種「設計是演化出來的、不是規劃出來的」態度,是 Part III 整個「重構邁向深層理解」的前奏。

討論問題

🎙️ 開場鉤子:「前面幾集我們一個一個拆 Entity、Value Object、Aggregate、Repository——這集 Evans 把它們全部丟進同一個貨運系統,讓你看這些零件怎麼咬合運轉。這是全書唯一的一次完整實戰。」

🎙️ 自問自答:「為什麼 Handling Event 要獨立成 Aggregate Root,但 Delivery History 不用?」——因為 Handling Event 有脫離 Cargo 的應用需求(查某趟船上所有裝卸),Delivery History 沒有、身分還是借來的。邊界跟著需求和身分走。

🎙️ 帶走的一題:「下次做設計,試試 Evans 的節奏:先做一個過得去的版本,然後拿幾個真實場景『走一遍』,看哪裡卡——卡住的地方,通常就是模型要重構的地方。」

更大範圍關聯

  • Part II 的整合與收尾:這章把所有戰術模式串成一套協同的語言。它是「讓你真正理解戰術 DDD 怎麼運作」的最佳一章——但也再次提醒,這仍在戰術層,下一個 Part 才進到讓 DDD 真正偉大的戰略思想。
  • DDDSample 開源實作:想從「聽概念」進到「看程式碼」的聽眾,DDDSample 是最權威的橋樑,直接對應這章的貨運系統。
  • 承先啟後:章末的 Anticorruption Layer 和「設計透過重構演化」,分別預告了戰略設計(第 10 集)和「重構邁向深層理解」(下一集起的整個 Part III)。
  • 連到 CQRS:「用查詢取代集合」這個重構,是 CQRS 讀寫分離思想的雛形,可作為想深入現代架構的聽眾的延伸線索。

錄製建議

  • 建議時長:約 22-24 分鐘。配比:初始模型+Entity/VO 分類約 6 分、Aggregate 邊界與 Repository 約 7 分、場景走查(變更目的地、重複業務)約 5 分、「用查詢取代集合」重構約 4 分、Module 反例+防腐層伏筆約 3 分。
  • 討論策略:這集適合走「跟著大師做一遍」的敘事,輕鬆一點、像在看 code review。別念物件清單,要把每個決策背後的「為什麼」講出來——因為這集真正要傳的是 Evans 的設計思路和節奏,不是貨運系統的細節。強力推薦 DDDSample 給想看 code 的聽眾。
  • 這集做完 Part II 收尾,可以做個小結(戰術模式全家福),然後鄭重預告:「接下來進入我認為這本書真正的精華——Part III 和 IV,從重構邁向深層理解,一路到戰略設計。」