DDD 戰略設計告訴你在哪裡切邊界——哪些業務概念屬於同一個 Bounded Context,哪些應該分開。戰術設計解決的是邊界內的問題:在一個 Bounded Context 裡,domain model 的構成要素怎麼建模?
這套詞彙(Entity、Value Object、Aggregate)不是為了讓你的類別結構更符合某種格式,而是讓 code 裡的概念和業務邏輯裡的概念對應起來——這樣當業務說「這個概念的邊界要調整」,你知道要動哪裡。
Entity vs Value Object:有沒有身分識別
戰術設計最基本的區分:一個 domain 物件有沒有獨立的身分識別(identity)?
Entity 有 identity,用 id 追蹤。即使所有屬性完全相同,兩個 Entity 是不同的——因為它們有不同的 id。訂單、用戶、產品是 Entity:order_id=123 和 order_id=456 永遠是兩個不同的訂單,即使商品清單、金額、收件地址全部相同。Entity 的狀態會隨時間改變,但身分不變。
Value Object 沒有 identity,用屬性值判斷相等。兩個 Money 值如果 amount=100, currency=TWD,它們在業務上是同一件事,可以互換,不需要 id 追蹤。地址、金額、顏色、座標都是 Value Object 的好候選——它們描述的是「什麼」,而不是「哪一個」。
Entity 例子:
Order { id: 123, items: [...], status: 'PENDING' } -- id 是 identity
User { id: 456, email: 'a@b.com', name: 'Terry' } -- id 是 identity
Value Object 例子:
Money { amount: 100, currency: 'TWD' } -- 沒有 id,兩個相同的 Money 可互換
Address { city: '台北', zip: '100', street: '...' } -- 不可變,描述值
Value Object 有一個重要性質:不可變(immutable)。你不是「修改」一個 Money,而是「換一個新的 Money」。這讓 Value Object 的行為可預測,副作用少。
Aggregate:邊界和一致性的單位
Entity 和 Value Object 不會孤立存在,它們會形成一個叢集(cluster)——這個叢集就是 Aggregate。
Aggregate 的設計有兩個核心規則:
規則一:Aggregate 是一致性邊界(consistency boundary)。 在一個 Aggregate 內部,業務規則的一致性要在一個 transaction 裡保證。跨 Aggregate 的一致性靠 eventual consistency,不靠 transaction。
規則二:外部只能透過 Aggregate Root 存取 Aggregate 內部。 訂單(Order)是 Aggregate Root,訂單明細(OrderItem)是 Aggregate 內部的 Entity。外部不能直接存取 OrderItem——要改訂單明細,必須透過 Order 的方法。這讓 Aggregate Root 可以保證業務規則不被繞過。
Order(Aggregate Root)
├── OrderItem 1 { productId, quantity, price }
├── OrderItem 2 { productId, quantity, price }
└── ShippingAddress(Value Object)
外部呼叫:
order.addItem(product, quantity) // ✅ 透過 Root
orderItem.updateQuantity(5) // ❌ 直接存取內部 Entity——不允許
Aggregate 邊界設計的常見錯誤
Aggregate 邊界設計是戰術設計裡最難的部分,因為「什麼應該在同一個 Aggregate 裡」沒有公式答案。
邊界設太大(God Aggregate):把所有相關概念都塞進一個 Aggregate,結果 Order 裡有用戶資訊、商品詳情、付款紀錄、物流資訊。每次更新都要鎖住整個 Aggregate,並發衝突頻繁。修一個配送地址,卻要載入整個訂單的 transaction context。
邊界設太小(每次操作都是多個 Aggregate):把本來應該在同一致性邊界內的概念拆開,結果每個業務操作都要跨多個 Aggregate——失去了「一致性邊界」的意義,變成每個業務步驟都在處理 eventual consistency。
判斷 Aggregate 邊界的實用問題:
- 這兩個概念的修改,業務上必須要 atomic 嗎?(必須 atomic → 放同一個 Aggregate)
- 一個 Aggregate 的實體在 load 時,另一個一定要一起 load 嗎?(不必 → 應該分開)
- 並發衝突的頻率高不高?(高 → 考慮拆開)
Domain Service:跨 Aggregate 的業務邏輯放哪
有些業務邏輯不屬於任何一個 Entity 或 Aggregate,比如「信用額度檢查」要同時查用戶的信用資訊和訂單歷史——它跨越了兩個 Aggregate。這種邏輯放哪?
不要把它硬塞進任一個 Entity 裡(這會讓 Entity 知道太多不屬於自己的事),也不要放在 Application Service(那層只協調流程,不包含業務規則)。正確的位置是 Domain Service:一個無狀態的 service,專門承接跨 Aggregate 的業務規則。
CreditCheckService.checkSufficientCredit(user, order)
// 跨 User Aggregate 和 Order Aggregate 的業務邏輯
// 不屬於 User,也不屬於 Order,屬於 Domain Service
Domain Service 的判斷標準:這段邏輯是業務規則(不是純技術操作),且它天然不屬於任何一個 Aggregate。符合這兩條才需要 Domain Service;否則很可能只是 Anemic Model 的症狀(把邏輯從 Entity 裡抽走,Entity 變成只有欄位的 data class)。
Domain Event:讓邊界說話
Aggregate 修改自己狀態的同時,可以發出 Domain Event 告知外部。Order 確認之後發出 OrderConfirmed,其他 Bounded Context(通知系統、積分系統)訂閱這個事件,自行決定要做什麼。
Domain Event 讓 Aggregate 之間保持解耦:Order 不需要知道「確認訂單之後誰要得到通知」,它只需要發出「訂單已確認」這個業務事實。這和 Event Sourcing 的事件是不同層次的概念——Domain Event 是業務通知,Event Sourcing 的事件是狀態的構成要素,雖然兩者可以組合但不互相依賴。
Factory 和 Repository:創建和查找
Factory:複雜的 Aggregate 創建邏輯不應該在 constructor 裡(那會讓依賴方了解太多實作細節)。Factory 封裝「如何建立一個合法的 Aggregate」的邏輯,確保創建出的 Aggregate 一開始就滿足業務規則。
Repository:提供 Aggregate 的持久化抽象。Repository 的介面屬於 domain 層,實作屬於 infrastructure 層——OrderRepository.findById(id) 對 domain 來說是「查訂單」,背後是 PostgreSQL 還是 Redis 不重要。Repository 以 Aggregate Root 為粒度操作(你保存/讀取的是整個 Aggregate,不是裡面的某個 OrderItem)。
戰術設計的實際使用建議
DDD 戰術設計的概念容易學,但正確使用需要對業務的深入理解。幾個實用方向:
從 Aggregate 邊界開始思考,不是從資料表開始思考。問題是「哪些概念必須在業務上一起保持一致」,不是「哪些 table 要 JOIN 在一起」。
Value Object 比你想像的常見。金額、地址、電話號碼、座標、時間區間——凡是「描述值而非識別物件」的概念,都值得考慮用 Value Object 而非 Entity。把這些概念建模成 Value Object 讓它們的業務語意更清楚,副作用也更可預測。
不需要一開始就把所有構件(Entity、VO、Aggregate、Domain Service)都用上。Domain 複雜度不夠高時,過度拆分反而模糊了業務邏輯。先讓 code 和業務語言對應起來,等複雜度自然冒出來再引入對應的構件。
上一篇 → DDD 戰略設計