軟體專案失敗不是因為糟糕的程式碼,而是因為誤解的需求。開發者建構他們認為業務需要的東西。業務利害關係人描述他們認為技術上可行的東西。業務語言與技術實作之間的鴻溝造成摩擦、延遲,以及解決錯誤問題的系統。
傳統軟體開發將資料庫視為宇宙的中心。設計從資料表和關聯開始。業務邏輯散落在預存程序、服務層和 UI 程式碼中。領域——核心業務問題——成為事後想法,埋藏在技術關注點之下。
領域驅動設計(DDD)顛覆了這種方法。它將領域模型置於中心,將業務邏輯視為系統最重要的部分。技術關注點——資料庫、框架、UI——成為服務領域的實作細節。業務和開發團隊使用直接出現在程式碼中的共享語言進行協作。
這種轉變聽起來簡單,但需要團隊對軟體思考方式的根本改變。DDD 引入了建模複雜業務邏輯的模式、管理大型系統的策略,以及保持程式碼與業務需求一致的實踐。理解 DDD 何時增加價值——以及何時更簡單的方法就足夠——決定了它是成為強大的工具還是過度工程的負擔。
本文追溯從資料庫中心到領域中心設計的演進,探索 DDD 的核心模式和實踐,檢視真實世界的應用,並提供何時採用這種方法的指導。
領域驅動設計時間軸
資料庫中心的問題
在 DDD 之前,大多數企業應用程式遵循資料庫中心的方法,造成了根本性的問題。
傳統資料庫優先設計
典型的開發流程從資料庫開始:
🚫 資料庫中心的問題
設計流程
- 從資料庫架構開始
- 建立資料表和關聯
- 產生資料存取程式碼
- 在上面添加業務邏輯
問題
- 資料庫結構驅動設計
- 業務邏輯散落各處
- 貧血領域模型(只有 getter/setter)
- 技術關注點主導
後果
- 程式碼不反映業務概念
- 變更需要資料庫遷移
- 業務規則隱藏在多個層次中
- 難以理解和維護
在這種方法中,開發者首先設計正規化的資料庫資料表。物件關聯映射(ORM)工具從資料表產生類別。業務邏輯被添加到任何方便的地方——預存程序、服務層、控制器或 UI 程式碼。結果系統沒有清晰的業務概念表示。
典型的電子商務系統可能有 Order、OrderItem 和 Customer 資料表。Order 類別成為具有 getter 和 setter 的資料容器。像「訂單超過 $100 免運費」這樣的業務規則散落在程式碼庫中。找到業務規則的實作位置需要搜尋多個檔案。
貧血領域模型反模式
資料庫中心設計產生貧血領域模型:
🚫 貧血領域模型
特徵
- 只有屬性的類別
- 領域物件中沒有業務邏輯
- 服務包含所有行為
- 物件只是資料容器
範例
public class Order {
private Long id;
private List<OrderItem> items;
private BigDecimal total;
// 只有 getter 和 setter
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
// ... 更多 getter/setter
}
**為什麼有問題**
- 違反物件導向原則
- 業務邏輯與資料分離
- 難以維護不變性
- 沒有封裝
貧血模型將物件視為資料結構而非行為實體。所有業務邏輯都存在於操作這些資料容器的服務類別中。這種偽裝成物件導向程式碼的程序式方法使系統更難理解和維護。
溝通鴻溝
資料庫中心設計擴大了業務與開發之間的鴻溝:
🚫 語言斷層
業務觀點
- 「客戶下訂單」
- 「訂單可以在出貨前取消」
- 「優質客戶獲得優先處理」
程式碼現實
- OrderService.createOrder()
- OrderRepository.updateStatus()
- CustomerTable.premiumFlag
結果
- 業務概念在程式碼中不可見
- 開發者在語言之間翻譯
- 誤解累積
- 知識隨時間流失
業務利害關係人使用業務術語描述領域。開發者使用技術術語實作。這些語言之間的翻譯引入錯誤,並使程式碼庫對非開發者來說難以理解。
領域驅動設計基礎
Eric Evans 2003 年的書《領域驅動設計》引入了一種處理複雜性的綜合方法。
核心哲學
DDD 的基礎建立在幾個關鍵原則上:
🎯 DDD 核心原則
領域優先
- 業務邏輯是最重要的部分
- 技術關注點服務領域
- 模型反映業務現實
- 程式碼說業務語言
通用語言
- 業務與開發者之間的共享詞彙
- 對話和程式碼中使用相同術語
- 減少翻譯錯誤
- 隨理解演進
迭代建模
- 模型透過協作改進
- 重構以獲得更深入的洞察
- 持續學習
- 程式碼和模型保持一致
DDD 將領域模型視為系統的核心。其他一切——資料庫、UI、外部服務——都是為了支援領域而存在。這種優先順序的顛倒改變了團隊處理設計的方式。
通用語言
最基本的 DDD 實踐是建立共享語言:
✅ 通用語言的好處
它是什麼
- 領域的共同詞彙
- 團隊中每個人都使用
- 直接出現在程式碼中
- 在模型中記錄
如何運作
- 業務:「客戶下訂單」
- 程式碼:
customer.placeOrder() - 不需要翻譯
- 立即理解
影響
- 減少誤解
- 使程式碼自我記錄
- 使業務能夠閱讀程式碼結構
- 揭示建模問題
當業務利害關係人說「下訂單」時,程式碼有一個 placeOrder() 方法。當他們討論「運送政策」時,程式碼有一個 ShippingPolicy 類別。會議中的語言與程式碼中的語言相符。
這種一致性具有深遠的影響。開發者停止在業務和技術術語之間翻譯。業務利害關係人可以審查類別圖並理解系統結構。業務理解與程式碼實作之間的不匹配立即變得可見。
豐富領域模型
DDD 提倡具有行為的豐富領域模型:
✅ 豐富領域模型
特徵
- 物件包含資料和行為
- 業務規則存在於領域物件中
- 封裝保護不變性
- 表達性、揭示意圖的方法
範例
public class Order {
private OrderId id;
private List<OrderLine> lines;
private OrderStatus status;
public void addLine(Product product, int quantity) {
if (status != OrderStatus.DRAFT) {
throw new IllegalStateException(
"無法修改已提交的訂單");
}
lines.add(new OrderLine(product, quantity));
}
public Money calculateTotal() {
return lines.stream()
.map(OrderLine::getSubtotal)
.reduce(Money.ZERO, Money::add);
}
}
**好處**
- 業務邏輯集中
- 不變性得到強制執行
- 自我記錄的程式碼
- 更容易測試和維護
豐富模型將業務規則封裝在領域物件中。Order 類別知道如何添加項目、計算總額和強制執行業務約束。業務邏輯不會散落在服務層中——它存在於應該存在的地方。
戰略設計模式
DDD 提供戰略模式來管理大型系統中的複雜性。
限界上下文
最重要的戰略模式是限界上下文:
🎯 限界上下文概念
定義
- 模型的明確邊界
- 在邊界內,術語具有精確含義
- 不同上下文可以有不同模型
- 透過分離減少複雜性
為什麼重要
- 「客戶」在不同上下文中意味著不同的事情
- 銷售上下文:客戶有訂單、信用額度
- 支援上下文:客戶有工單、歷史記錄
- 運送上下文:客戶有送貨地址
好處
- 每個上下文保持專注
- 團隊可以獨立工作
- 模型保持連貫
- 防止「一個模型統治一切」
大型系統無法擁有單一統一模型。「客戶」一詞對銷售、支援和運送團隊意味著不同的事情。試圖建立一個滿足所有上下文的 Customer 類別會產生臃腫、不連貫的模型。
限界上下文透過明確分離模型來解決這個問題。每個上下文都有自己針對其需求優化的模型。銷售上下文有一個具有訂單歷史的 Customer。支援上下文有一個具有支援工單的 Customer。這些是不同的模型,這沒問題。
- 訂單
- 信用額度
- 付款條件] end subgraph Support["支援上下文"] SuC[客戶
- 工單
- 支援歷史
- 優先級] end subgraph Shipping["運送上下文"] ShC[客戶
- 送貨地址
- 運送偏好
- 配送歷史] end Sales -.->|上下文映射| Support Sales -.->|上下文映射| Shipping Support -.->|上下文映射| Shipping style Sales fill:#e1f5ff,stroke:#333,stroke-width:2px style Support fill:#fff4e1,stroke:#333,stroke-width:2px style Shipping fill:#e8f5e9,stroke:#333,stroke-width:2px
上下文映射
限界上下文必須整合,需要上下文映射:
🗺️ 上下文映射模式
合作夥伴
- 兩個上下文緊密協作
- 團隊協調變更
- 共享成功標準
客戶-供應商
- 上游上下文提供資料
- 下游上下文消費
- 正式介面協議
順從者
- 下游順從上游模型
- 當上游不會改變時使用
- 接受他們的模型
防腐層
- 在上下文之間翻譯
- 保護領域模型免受外部影響
- 隔離遺留系統
共享核心
- 上下文之間的小型共享模型
- 需要協調
- 謹慎使用
上下文映射定義限界上下文如何關聯。防腐層保護你的領域模型免受外部系統影響。客戶-供應商關係建立明確的責任。這些模式使整合明確且可管理。
聚合
聚合定義一致性邊界:
📦 聚合模式
定義
- 作為單元處理的物件集群
- 一個實體是聚合根
- 外部引用僅指向根
- 在邊界內強制執行一致性
規則
- 根實體具有全域識別
- 內部實體具有本地識別
- 外部物件不能持有對內部的引用
- 變更透過根進行
範例
- Order 是聚合根
- OrderLine 是內部實體
- 外部程式碼引用 Order,而非 OrderLine
- Order 確保所有行的一致性
聚合防止「大泥球」,即所有東西都引用所有東西。透過定義明確的邊界和存取規則,聚合使系統更易於維護並支援分散式交易。
戰術設計模式
DDD 提供戰術模式來實作領域模型。
建構區塊
戰術模式形成領域模型的詞彙:
🧱 DDD 建構區塊
實體
- 具有識別的物件
- 識別隨時間持續
- 可變狀態
- 範例:Customer、Order
值物件
- 由屬性定義的物件
- 沒有識別
- 不可變
- 範例:Money、Address、DateRange
服務
- 不屬於實體的操作
- 無狀態
- 領域操作
- 範例:PricingService、ShippingCalculator
儲存庫
- 持久化的抽象
- 類似集合的介面
- 隱藏資料庫細節
- 範例:OrderRepository
工廠
- 複雜物件建立
- 封裝建構邏輯
- 確保有效物件
- 範例:OrderFactory
這些模式提供了組織領域邏輯的結構化方式。實體具有識別和生命週期。值物件表示沒有識別的概念。服務處理跨越多個物件的操作。儲存庫抽象持久化。工廠處理複雜的建立。
實體 vs 值物件
理解區別至關重要:
🔍 實體 vs 值物件
實體範例:Customer
public class Customer {
private CustomerId id; // 識別
private String name;
private Email email;
// 基於識別的相等性
public boolean equals(Object o) {
if (!(o instanceof Customer)) return false;
Customer other = (Customer) o;
return id.equals(other.id);
}
}
**值物件範例:Money**
public class Money {
private final BigDecimal amount;
private final Currency currency;
// 不可變
public Money add(Money other) {
if (!currency.equals(other.currency)) {
throw new IllegalArgumentException(
"無法加總不同貨幣");
}
return new Money(
amount.add(other.amount),
currency);
}
// 基於值的相等性
public boolean equals(Object o) {
if (!(o instanceof Money)) return false;
Money other = (Money) o;
return amount.equals(other.amount)
&& currency.equals(other.currency);
}
}
實體透過識別進行比較——兩個具有相同名稱的客戶如果有不同的 ID 就是不同的。值物件透過值進行比較——兩個具有相同金額和貨幣的 Money 物件是相同的。
領域事件
領域事件捕捉重要的業務發生:
📢 領域事件
目的
- 表示發生的事情
- 過去式命名
- 不可變
- 實現鬆耦合
範例
public class OrderPlaced {
private final OrderId orderId;
private final CustomerId customerId;
private final Instant occurredAt;
public OrderPlaced(OrderId orderId,
CustomerId customerId) {
this.orderId = orderId;
this.customerId = customerId;
this.occurredAt = Instant.now();
}
}
**好處**
- 明確的業務事件
- 解耦的元件
- 稽核軌跡
- 支援事件溯源
領域事件使隱含概念變得明確。系統不是默默更新狀態,而是發布 OrderPlaced 事件。系統的其他部分可以反應——發送確認電子郵件、更新庫存、觸發運送。事件實現鬆耦合並提供自然的稽核軌跡。
真實世界應用
DDD 在特定情境中表現出色,但並非總是正確的選擇。
何時 DDD 增加價值
DDD 最適合複雜領域:
✅ 良好的 DDD 候選者
複雜業務邏輯
- 許多業務規則
- 規則以複雜方式互動
- 需要領域專家
- 範例:保險承保、交易系統
協作建模
- 業務專家可用
- 可能進行迭代精煉
- 共享理解有價值
- 範例:客製化企業應用程式
長期系統
- 系統將演進多年
- 可維護性至關重要
- 知識保存重要
- 範例:核心業務系統
戰略差異化
- 領域是競爭優勢
- 客製化邏輯,而非通用 CRUD
- 業務規則中的創新
- 範例:推薦引擎、定價演算法
當領域複雜性證明其合理性時,DDD 的開銷是值得的。具有複雜業務規則、多個利害關係人和長生命週期的系統受益於 DDD 的建模嚴謹性。
何時更簡單的方法就足夠
並非每個系統都需要 DDD:
⚠️ DDD 可能過度
簡單 CRUD 應用程式
- 基本的建立、讀取、更新、刪除
- 最少的業務邏輯
- 資料管理焦點
- 更好的方法:簡單分層架構
技術問題
- 演算法密集型系統
- 基礎設施工具
- 沒有複雜領域
- 更好的方法:技術設計模式
原型和 MVP
- 速度優於結構
- 不確定的需求
- 可能被丟棄
- 更好的方法:快速開發框架
沒有領域專家的小團隊
- 沒有人可以協作
- 有限的領域知識
- 無法建立通用語言
- 更好的方法:更簡單的模式
具有基本 CRUD 操作的內容管理系統不需要 DDD。測試市場適應性的原型不應投資於精細的領域建模。DDD 的好處伴隨著成本——複雜性、學習曲線和開發時間。
電子商務平台範例
考慮電子商務平台的訂單管理:
🛒 電子商務領域模型
限界上下文
- 目錄:產品、類別、搜尋
- 購物:購物車、結帳、付款
- 訂單管理:訂單、履行、追蹤
- 客戶:帳戶、偏好、歷史
關鍵聚合
- Order(根:Order,包含:OrderLine)
- ShoppingCart(根:Cart,包含:CartItem)
- Product(根:Product,包含:Variant)
領域事件
- OrderPlaced
- PaymentProcessed
- OrderShipped
- OrderCancelled
值物件
- Money(金額 + 貨幣)
- Address(街道、城市、郵遞區號)
- ProductSku(識別碼)
這種結構使業務概念明確。Order 聚合確保一致性——你不能有沒有訂單的訂單行。領域事件實現整合——當 OrderPlaced 觸發時,庫存更新並發送電子郵件。通用語言貫穿始終——業務利害關係人和開發者使用相同的術語。
金融服務範例
交易系統展示了 DDD 的力量:
💰 交易系統領域
複雜業務規則
- 每個交易員的部位限制
- 風險計算
- 法規遵循
- 市場時間和假日
豐富領域模型
public class Trade {
public void execute() {
if (!market.isOpen()) {
throw new MarketClosedException();
}
if (exceedsPositionLimit()) {
throw new PositionLimitException();
}
if (!passesRiskCheck()) {
throw new RiskLimitException();
}
// 執行交易
}
}
**好處**
- 業務規則集中
- 在程式碼中強制執行合規性
- 領域專家可以審查邏輯
- 變更追溯到業務需求
金融系統具有複雜、不斷演進的規則。DDD 對領域模型的關注使這種複雜性保持可管理。當法規改變時,領域模型改變。程式碼反映當前的業務理解。
實作策略
採用 DDD 需要實用的策略。
開始使用 DDD
從戰略模式開始:
💡 DDD 採用路徑
階段 1:戰略設計
- 識別限界上下文
- 建立上下文映射
- 建立通用語言
- 定義核心領域
階段 2:戰術模式
- 建模關鍵聚合
- 識別實體和值物件
- 定義領域事件
- 實作儲存庫
階段 3:精煉
- 重構以獲得更深入的洞察
- 演進通用語言
- 調整邊界
- 改進模型
從戰略設計開始以理解大局。在深入戰術模式之前識別限界上下文。這可以防止過早優化並確保努力集中在核心領域。
Event Storming
Event Storming 促進協作建模:
🎨 Event Storming 流程
它是什麼
- 基於工作坊的建模技術
- 在牆上使用便利貼
- 協作和視覺化
- 快速領域探索
步驟
- 識別領域事件(橙色便利貼)
- 添加觸發事件的命令(藍色便利貼)
- 識別聚合(黃色便利貼)
- 找到限界上下文(邊界)
- 發現問題和機會(紅色便利貼)
好處
- 吸引整個團隊
- 揭示隱藏的複雜性
- 建立共享理解
- 快速且有效
Event Storming 將業務專家和開發者聚集在一起探索領域。視覺化、協作的性質快速浮現假設和分歧。幾個小時的 Event Storming 可以揭示透過傳統需求收集需要數週才能發現的洞察。
避免常見陷阱
DDD 有眾所周知的反模式:
⚠️ DDD 反模式
貧血領域模型
- 問題:沒有行為的物件
- 解決方案:將邏輯移入領域物件
上帝聚合
- 問題:聚合太大
- 解決方案:拆分成更小的聚合
缺少限界上下文
- 問題:一個模型適用於所有事物
- 解決方案:識別並分離上下文
忽略通用語言
- 問題:程式碼使用技術術語
- 解決方案:重構以匹配業務語言
過度工程簡單領域
- 問題:對 CRUD 應用程式使用 DDD
- 解決方案:使用更簡單的方法
最常見的錯誤是在不理解其目的的情況下應用 DDD 模式。聚合變得臃腫。通用語言被忽略。領域模型變得貧血。成功需要紀律和持續重構以獲得更深入的洞察。
DDD 與現代架構
DDD 影響當代架構模式。
微服務與限界上下文
限界上下文自然映射到微服務:
🔗 DDD + 微服務
對齊
- 每個微服務是一個限界上下文
- 明確的邊界和責任
- 獨立部署
- 團隊所有權
好處
- DDD 提供服務邊界
- 防止分散式單體
- 實現自主團隊
- 自然的服務分解
挑戰
- 分散式交易
- 資料一致性
- 整合複雜性
- 營運開銷
沒有限界上下文的微服務經常失敗——服務有不明確的邊界和緊密耦合。DDD 的戰略模式提供有原則的服務分解。每個限界上下文成為具有明確領域焦點的微服務。
事件溯源與 CQRS
DDD 與事件溯源和 CQRS 配合良好:
📊 事件溯源 + CQRS
事件溯源
- 儲存領域事件,而非當前狀態
- 透過重播事件重建狀態
- 完整的稽核軌跡
- 時間旅行除錯
CQRS(命令查詢責任分離)
- 分離讀取和寫入模型
- 獨立優化每個
- 讀取/寫入使用不同資料庫
- 最終一致性
與 DDD 整合
- 領域事件是一等公民
- 聚合產生事件
- 讀取模型服務查詢
- 寫入模型強制執行不變性
事件溯源使領域事件成為真相來源。CQRS 分離命令處理(寫入)和查詢(讀取)。與 DDD 一起,它們建立業務事件明確、可稽核並驅動整個架構的系統。
六角架構
DDD 自然適合六角(埠與配接器)架構:
實體、值物件
聚合、服務] end subgraph Ports["埠"] IP[輸入埠
使用案例] OP[輸出埠
儲存庫、服務] end subgraph Adapters["配接器"] REST[REST API] UI[Web UI] DB[資料庫] MSG[訊息佇列] end REST --> IP UI --> IP IP --> DM DM --> OP OP --> DB OP --> MSG style Core fill:#e1f5ff,stroke:#333,stroke-width:3px style Ports fill:#fff4e1,stroke:#333,stroke-width:2px style Adapters fill:#e8f5e9,stroke:#333,stroke-width:2px
🏛️ 六角架構 + DDD
結構
- 領域模型在中心
- 埠定義介面
- 配接器實作技術細節
- 依賴指向內部
好處
- 領域與基礎設施隔離
- 易於測試領域邏輯
- 交換實作
- 技術無關的核心
六角架構使領域模型獨立於技術關注點。資料庫、框架和外部服務成為實作細節。領域保持純粹,專注於業務邏輯。
衡量成功
DDD 的價值出現在特定結果中:
✅ DDD 成功指標
溝通
- 業務和開發者使用相同術語
- 更少的誤解
- 更快的需求澄清
- 程式碼審查包括業務利害關係人
可維護性
- 業務邏輯易於找到
- 變更局限於聚合
- 重構不會破壞一切
- 新開發者快速理解
靈活性
- 業務規則變更簡單明瞭
- 新功能自然契合
- 技術變更不影響領域
- 系統隨業務演進
品質
- 業務邏輯中的錯誤更少
- 不變性得到強制執行
- 邊緣案例得到處理
- 領域測試可讀
成功不是透過模式採用來衡量,而是透過業務結果。業務利害關係人能理解程式碼結構嗎?變更需要更少時間嗎?系統更可靠嗎?這些指標揭示 DDD 是否提供價值。
結論
領域驅動設計代表從資料庫中心到領域中心軟體開發的根本轉變。透過將領域模型置於中心、建立通用語言,以及應用戰略和戰術模式,DDD 建立與業務需求一致且隨時間保持可維護的系統。
從傳統方法到 DDD 的旅程揭示了重要的教訓:
複雜性需要結構:簡單的 CRUD 應用程式不需要 DDD。具有複雜業務規則的複雜領域受益於 DDD 的建模嚴謹性。關鍵是將方法與問題複雜性相匹配。
語言很重要:通用語言不僅僅是錦上添花——它是基礎。當業務和開發者共享詞彙時,誤解減少,程式碼變得自我記錄。維護這種共享語言的紀律持續帶來回報。
邊界實現規模:限界上下文防止「一個模型統治一切」的陷阱。透過明確分離關注點,系統保持可理解,團隊可以獨立工作。隨著系統成長,這變得至關重要。
模式服務目的:DDD 的模式——聚合、實體、值物件、領域事件——不是貨物崇拜實踐。每個都解決特定問題。理解它們解決的問題可以防止誤用。
協作驅動品質:當業務專家和開發者持續協作時,DDD 效果最好。Event Storming 和其他協作建模技術比傳統需求文件更快地浮現假設並建立共享理解。
採用 DDD 的決定應該是深思熟慮的。對於具有複雜業務邏輯、長生命週期和可用領域專家的系統,DDD 提供多年來回報的結構。對於更簡單的系統、原型或技術問題,更輕量的方法就足夠了。
現代架構模式——微服務、事件溯源、CQRS、六角架構——自然與 DDD 原則一致。限界上下文提供服務邊界。領域事件實現事件溯源。領域模型保持獨立於技術關注點。
DDD 不是銀彈。它需要投資、紀律和持續重構以獲得更深入的洞察。但對於正確的問題,它將軟體開發從在業務和技術語言之間翻譯轉變為建立直接說業務語言的系統。
成功的最終衡量標準是軟體是否有效解決真實業務問題,並能隨著這些問題的變化而演進。DDD 提供工具和實踐來實現這一目標,但只有在深思熟慮地應用於其複雜性合理的領域時才有效。