領域驅動設計 (DDD) — 模式與實踐 是做什麼的?
何時使用 DDD
使用時機:
- 超越 CRUD 的複雜業務邏輯
- 多位領域專家使用不同詞彙
- 領域模型是競爭優勢
- 企業級應用程式
不使用的時機:
- 簡單的 CRUD 應用程式
- 黑客松 / MVP(過於繁重)
- 沒有領域專家可用
策略設計
限界上下文 (Bounded Contexts)
DDD 中最重要的一個概念。限界上下文是一個邊界,在其中定義並適用特定的模型。
範例 — 電子商務:
[訂單上下文] [付款上下文] [運送上下文]
- 訂單 (Order) - 付款 (Payment) - 運送 (Shipment)
- 訂單項目 (OrderItem) - 交易 (Transaction) - 追蹤編號 (TrackingNumber)
- 客戶 (Customer, ref)- 退款 (Refund) - 地址 (Address)
- 地址 (Address, value)- 發票 (Invoice) - 運送商 (Carrier)
Customer 在各個上下文中含義不同:
- 訂單上下文:姓名、電子郵件、運送偏好
- 付款上下文:帳單資訊、付款方式
- 支援上下文:工單歷史、滿意度分數
上下文映射 (Context Map)
[訂單] ←→ [付款] # 合作夥伴關係 (Partnership)
[訂單] → [運送] # 客戶-供應商 (Customer-Supplier)
[訂單] → [舊版CRM] # 防腐層 (Anti-Corruption Layer)
戰術設計
實體 (Entities)
具有身份的物件。兩個屬性相同但 ID 不同的實體是不同的。
class User {
readonly id: UserId;
name: string;
email: Email; // 值物件
}
值物件 (Value Objects)
由其屬性而非身份定義的物件。不可變。
class Email {
constructor(readonly value: string) {
if (!isValidEmail(value)) throw new InvalidEmailError(value);
}
equals(other: Email): boolean {
return this.value === other.value;
}
}
聚合 (Aggregates)
由單一根實體(聚合根)組成的實體和值物件的叢集。所有存取都通過根進行。
class Order { // 聚合根
private items: OrderItem[] = [];
addItem(product: ProductRef, quantity: number): void {
// 業務邏輯放在此處,不在服務中
if (quantity <= 0) throw new InvalidQuantityError();
this.items.push(new OrderItem(product, quantity));
}
get total(): Money {
return this.items.reduce((sum, item) => sum.add(item.subtotal), Money.zero());
}
}
聚合規則:
- 僅透過 ID 引用其他聚合
- 每次交易僅操作一個聚合
- 聚合間最終一致性
領域事件 (Domain Events)
在領域中發生的、領域專家關心的某些事件。
class OrderPlaced implements DomainEvent {
constructor(
readonly orderId: OrderId,
readonly customerId: CustomerId,
readonly total: Money,
readonly occurredAt: Date
) {}
}
領域服務 (Domain Services)
不自然歸屬於實體或值物件的業務邏輯。
class PricingService {
calculatePrice(order: Order, customer: Customer, promotions: Promotion[]): Money {
// 涉及多個聚合的複雜定價邏輯
}
}
儲存庫 (Repositories)
資料存取的抽象。每個聚合根一個儲存庫。
interface OrderRepository {
findById(id: OrderId): Promise<Order | null>;
save(order: Order): Promise<void>;
delete(id: OrderId): Promise<void>;
}
目錄結構 (DDD)
src/
├── modules/
│ └── orders/ # 限界上下文
│ ├── domain/
│ │ ├── entities/
│ │ │ └── order.ts # 聚合根
│ │ ├── value-objects/
│ │ │ └── money.ts
│ │ ├── events/
│ │ │ └── order-placed.ts
│ │ ├── services/
│ │ │ └── pricing.ts
│ │ └── repositories/
│ │ └── order.repository.ts # 介面
│ ├── application/
│ │ ├── commands/
│ │ │ └── place-order.ts
│ │ ├── queries/
│ │ │ └── get-order.ts
│ │ └── handlers/
│ │ └── place-order.handler.ts
│ └── infrastructure/
│ ├── persistence/
│ │ └── order.repository.impl.ts # 實作
│ └── api/
│ └── orders.controller.ts
Clean Architecture 依賴規則
在 DDD 中,依賴關係必須嚴格指向內層:
[ 框架與驅動 (Web, DB, UI) ]
└──▶ [ 介面適配器 (Controllers, Gateways) ]
└──▶ [ 應用層 (Use Cases, CQRS Handlers) ]
└──▶ [ 領域層 (Entities, Value Objects) ]
- 領域層對 ORM(Prisma, TypeORM)、HTTP 框架(Express, NestJS)或外部 SDK 零依賴。
- 儲存庫在領域層/應用層中定義為介面,並在基礎設施層中實作。
領域事件 vs 整合事件
- 領域事件:表示單一限界上下文內部的狀態變更。
- 直接在聚合根內部引發(
order.addItem(...)引發OrderItemAdded)。 - 在交易提交前於處理程序中分派。
- 直接在聚合根內部引發(
- 整合事件:跨限界上下文邊界發布,以與其他服務通訊。
- 透過事務性發件箱模式分派到訊息代理。
- 必須使用向後相容的結構化版本。
防腐層 (Anti-Corruption Layer, ACL)
從外部限界上下文或第三方供應商 API(例如 Stripe, Salesforce)消費資料時:
- 絕對不要將外部領域模型直接匯入您的領域。
- 在基礎設施層建立 ACL 轉換器/適配器,將外部 DTO 轉換為您自己的值物件和實體。
反模式
- [失敗] 貧血領域模型 — 實體僅有 getter/setter,所有邏輯都在服務中
- [失敗] 巨型聚合 — 聚合應該小而專注於不變量
- [失敗] 跨聚合交易 — 使用最終一致性
- [失敗] 到處使用 DDD — 僅在複雜度證明合理時使用 DDD
- [失敗] ORM 實體洩漏到領域中 — 領域實體不得依賴
@Entity()或 ORM 裝飾器
程式碼範例
詳見 EXAMPLES.md 中的詳細程式碼範例。
驗證檢查清單
在完成任務前檢討階段應驗證的項目。
常見錯誤
要明確避免的反模式和事項。詳見 TROUBLESHOOTING.md。
整合說明
此技能如何與其他技能互動。