Fab Forward Dev/

ナレッジベース

製造業の一般知識

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 参照に置き換え