DDD 戦術パターン / DDD Tactical Design Patterns
エンティティ、値オブジェクト、集約、リポジトリ、ドメインサービスの使い分け。
→ DDD 対応:
ddd/03-aggregates/(我々のシステムでの適用),ddd/06-value-objects.md
1. ビルディングブロック一覧
| パターン | 英語 | 役割 | 同一性 |
|---|---|---|---|
| エンティティ | Entity | 一意の ID を持つドメインオブジェクト | ID で識別 |
| 値オブジェクト | Value Object | 不変の値。属性の組合せで表現 | 値で等価判定 |
| 集約 | Aggregate | 整合性の境界。ルートエンティティ + 子 | ルート ID |
| リポジトリ | Repository | 集約の永続化・取得 | — |
| ドメインサービス | Domain Service | エンティティに属さないドメインロジック | — |
| ドメインイベント | Domain Event | 「何かが起きた」の記録 | event_id |
| ファクトリ | Factory | 複雑なオブジェクトの生成ロジック | — |
2. エンティティ vs 値オブジェクト
判断基準
| 質問 | エンティティ | 値オブジェクト |
|---|---|---|
| ID で追跡する必要があるか? | はい | いいえ |
| ライフサイクルがあるか? | はい (生成→変更→削除) | いいえ (不変) |
| 同じ属性値でも区別するか? | はい (別の ID = 別物) | いいえ (同じ値 = 同じもの) |
製造業の例
| ドメイン概念 | エンティティ? | 理由 |
|---|---|---|
| 受注 | エンティティ | ORDER_NO で識別。ライフサイクルあり |
| 得意先 | エンティティ | TOKUISAKI_CODE で識別 |
| 金額 (¥100,000) | 値オブジェクト | ¥100,000 は ¥100,000。ID 不要 |
| 住所 | 値オブジェクト | 同じ住所は同じ。変更は新しい値を作る |
| 受入実績 | エンティティ | 個別に追跡が必要 |
3. 集約の設計ルール
ルール 1: 真の不変条件だけを集約内に
集約は「1 つのトランザクションで保証すべき整合性の範囲」。
✅ 見積 + 見積明細 → 合計金額の整合性 (同一トランザクション)
❌ 見積 + 受注 → 別々のタイミングで変更される (別集約)
ルール 2: 集約は小さく保つ
大きい集約 = ロック競合 = パフォーマンス問題。
❌ 受注集約 { 手配[], 製作指示[], 検収[], 売上, 日報[] } ← 大きすぎ
✅ 受注集約 { 注文書 }
手配集約 { → 受注への参照 }
製作指示集約 { 日報[] }
ルール 3: 集約間は ID で参照
❌ purchaseOrder.salesOrder.customer.name ← オブジェクト参照
✅ purchaseOrder.orderNumber → SalesOrder を別途取得 ← ID 参照
ルール 4: 集約外は結果整合性
集約間の整合性はドメインイベント + 結果整合性 (Eventual Consistency) で実現。
受注確定 → OrderAccepted イベント発行
→ 手配サービスが受信 → PurchaseOrder を自動生成
→ 製作指示サービスが受信 → ProductionInstruction を自動生成
4. リポジトリ
原則
- 集約ルートに対して 1 リポジトリ (子エンティティ用のリポジトリは作らない)
- コレクションのように振る舞う (add, find, remove)
- 永続化の詳細を隠蔽 (SQL, NoSQL はリポジトリの中に閉じ込める)
インターフェース例
interface SalesOrderRepository {
findByOrderNumber(orderNumber: string): SalesOrder | null;
findByCustomer(customerCode: string): SalesOrder[];
save(order: SalesOrder): void;
}
アンチパターン
❌ QuotationLineRepository ← 子エンティティ用リポジトリ
❌ repository.findAll() ← 全件取得は集約の目的ではない
❌ repository.update(field) ← 部分更新。集約全体を save する
5. ドメインサービス
いつ使うか
エンティティにも値オブジェクトにも自然に属さないドメインロジック。
| 例 | なぜドメインサービスか |
|---|---|
| 原価計算 | 受入 + 日報 + 検収の横断的な集計 |
| 見積→受注変換 | 2 つの集約をまたぐ操作 |
| 顧客コードの重複チェック | 外部リソース (DB) へのアクセスが必要 |
注意点
- ドメインサービスに ロジックを集めすぎない (貧血ドメインモデルの兆候)
- まずエンティティ/値オブジェクトにロジックを置けないか検討
- ドメインサービスは ステートレス (状態を持たない)
6. バウンデッドコンテキスト統合パターン
| パターン | 説明 | いつ使うか |
|---|---|---|
| Shared Kernel | 共有コードを両チームで管理 | 小チーム、密結合OK |
| Customer-Supplier | 上流が下流に合わせる | 上流チームが協力的 |
| Conformist | 下流が上流に従う | 上流を変更できない |
| ACL (Anti-Corruption Layer) | 変換層を設ける | 異なるドメインモデルの統合 |
| Open Host Service | 公開 API を提供 | 多数の消費者がいる |
| Published Language | 標準フォーマットで共有 | JSON Schema, Protobuf |
我々のシステムでの適用
Sales CRM ──(Conformist)──→ Integration Contracts
Integration Contracts ──(ACL)──→ PMS
Sales は Contracts の仕様に準拠 (Conformist)
PMS は ACL で自ドメインモデルに変換して取り込む
7. よくある間違いと対策
| 間違い | 症状 | 対策 |
|---|---|---|
| 巨大な集約 | 1 つのトランザクションで大量のデータをロック | 集約を分割、イベントで連携 |
| 貧血ドメインモデル | エンティティが getter/setter だけ | ロジックをエンティティに移動 |
| CRUD 中心の設計 | Repository が SQL の薄いラッパー | ユースケース中心に再設計 |
| 全部エンティティ | 値オブジェクトがゼロ | Money, Address 等を値オブジェクト化 |
| 集約間のオブジェクト参照 | N+1 クエリ、循環参照 | ID 参照に置き換え |