戦術的設計の全体像

前章で学んだ戦略的設計がシステム全体の「大きな絵」を描くのに対し、 戦術的設計(Tactical Design)はBounded Context内部でドメインモデルを精密に実装するためのパターン群です。 Eric EvansはこれらをDDDの「ビルディングブロック」と呼びました。

graph TB
  AR[集約ルート\nAggregate Root] --> E[エンティティ\nEntity]
  AR --> VO[値オブジェクト\nValue Object]
  E --> VO
  AR -.->|発行| DE[ドメインイベント\nDomain Event]
  R[リポジトリ\nRepository] -->|永続化/取得| AR
  DS[ドメインサービス\nDomain Service] -->|操作| AR
  F[ファクトリ\nFactory] -->|生成| AR

  style AR fill:#8b5cf6,stroke:#6d28d9,color:#fff
  style E fill:#3b82f6,stroke:#1d4ed8,color:#fff
  style VO fill:#14b8a6,stroke:#0d9488,color:#fff
  style DE fill:#f97316,stroke:#ea580c,color:#fff
  style R fill:#6366f1,stroke:#4f46e5,color:#fff
  style DS fill:#6366f1,stroke:#4f46e5,color:#fff
  style F fill:#6366f1,stroke:#4f46e5,color:#fff
DDDの戦術的設計パターンの関係図: 集約ルートを中心に、エンティティ・値オブジェクト・ドメインイベント・リポジトリ・ドメインサービス・ファクトリが連携する

エンティティ(Entity) — IDで同一性が決まるオブジェクト

エンティティは、一意の識別子(ID)を持ち、時間の経過とともに状態が変化しても同一性が維持されるオブジェクトです。 名前や住所が変わっても同じ「人」であり続けるように、属性が変わってもIDが同じなら同一エンティティです。

重要なのは、エンティティは単なるデータの入れ物ではなく、ビジネスロジックをカプセル化するということです。 getter/setterだけのエンティティは「貧血ドメインモデル」と呼ばれるアンチパターンです。

// 悪い例: 貧血ドメインモデル(getter/setterだけ)
class Order {
  id: string;
  status: string;
  items: OrderItem[];
  // ビジネスロジックがない...
}

// 良い例: リッチなドメインモデル
class Order {
  private readonly id: OrderId;
  private status: OrderStatus;
  private items: OrderItem[];

  addItem(item: OrderItem): void {
    if (this.status !== OrderStatus.DRAFT) {
      throw new Error('確定済みの注文には商品を追加できません');
    }
    this.items.push(item);
  }

  confirm(): void {
    if (this.items.length === 0) {
      throw new Error('商品がない注文は確定できません');
    }
    this.status = OrderStatus.CONFIRMED;
  }
}

値オブジェクト(Value Object) — 属性値で等価性が決まる不変オブジェクト

値オブジェクトは、識別子を持たず、属性の値のみで定義される不変(immutable)オブジェクトです。 同じ属性を持つ2つの値オブジェクトは等価として扱えます。

たとえば「100円」と「100円」は同じ価値です。どちらの「100円」かを区別する必要はありません。 これが値オブジェクトの本質です。

観点 エンティティ 値オブジェクト
同一性 IDで判定 属性値で判定
可変性 状態が変化する 不変(immutable)
ライフサイクル 生成・変更・削除がある 更新ではなく新規インスタンスで置き換え
顧客、注文、銀行口座 住所、金額、日付範囲、メールアドレス
// 値オブジェクトの例: Money
class Money {
  private constructor(
    readonly amount: number,
    readonly currency: string
  ) {
    Object.freeze(this);
  }

  static create(amount: number, currency: string): Money {
    if (amount < 0) throw new Error('金額は0以上である必要があります');
    return new Money(amount, currency);
  }

  // 副作用なし: 新しいMoneyを返す
  add(other: Money): Money {
    if (this.currency !== other.currency) {
      throw new Error('異なる通貨は加算できません');
    }
    return Money.create(this.amount + other.amount, this.currency);
  }

  equals(other: Money): boolean {
    return this.amount === other.amount
      && this.currency === other.currency;
  }
}

集約(Aggregate) — トランザクション整合性の境界

集約(Aggregate)は、DDDの中でも最も重要かつ設計が難しいパターンです。 関連するエンティティと値オブジェクトを1つのまとまりとして扱い、トランザクション整合性の境界を定義します。

集約には必ず1つの集約ルート(Aggregate Root)があり、外部からのアクセスは集約ルートを通じてのみ行います。 集約内部のエンティティや値オブジェクトに直接アクセスすることはできません。

graph TB
  subgraph aggregate[注文集約]
    OR[注文\n集約ルート] --> OI1[注文明細1]
    OR --> OI2[注文明細2]
    OR --> SA[配送先住所\n値オブジェクト]
    OI1 --> P1[商品ID\n値オブジェクト]
    OI2 --> P2[商品ID\n値オブジェクト]
  end
  EXT[外部] -->|集約ルート経由のみ| OR
  EXT -.->|直接アクセス禁止| OI1

  style aggregate fill:#1e1e2e,stroke:#8b5cf6,color:#fff
  style OR fill:#8b5cf6,stroke:#6d28d9,color:#fff
  style OI1 fill:#3b82f6,stroke:#1d4ed8,color:#fff
  style OI2 fill:#3b82f6,stroke:#1d4ed8,color:#fff
  style SA fill:#14b8a6,stroke:#0d9488,color:#fff
  style P1 fill:#14b8a6,stroke:#0d9488,color:#fff
  style P2 fill:#14b8a6,stroke:#0d9488,color:#fff
  style EXT fill:#6366f1,stroke:#4f46e5,color:#fff
注文集約の構造: 集約ルート(注文)を通じてのみ外部からアクセスし、内部の注文明細や配送先住所は直接操作しない

集約の設計ルール

Vaughn Vernonは集約設計の4つのルールを提唱しています。

  1. 小さく設計する: 単一トランザクション内で一貫性が必要なデータのみ含める
  2. 他の集約はIDのみで参照する: 直接のオブジェクト参照を持たない
  3. 集約間は結果整合性を使う: ドメインイベントで非同期に連携する
  4. リポジトリは集約ルートに対してのみ定義する

リポジトリ(Repository) — 永続化の抽象化

リポジトリは、集約の永続化と取得を抽象化するインターフェースです。 ドメイン層からインフラストラクチャ(データベース等)の詳細を隠蔽し、集約をコレクションのように扱えるようにします。

リポジトリは集約ルートに対してのみ定義します。集約内部のエンティティへの個別リポジトリは不要です。

ドメインサービス — 複数の集約にまたがるロジック

ドメインサービスは、エンティティや値オブジェクトに自然に属さないビジネスロジックを担うステートレスなオブジェクトです。 複数のエンティティや集約にまたがるビジネスルールをカプセル化します。

ドメインサービス アプリケーションサービス
役割 複数集約にまたがるビジネスルール ユースケースのオーケストレーション(調整役)
ビジネスロジック 含む 含まない
配送スケジューリング(空き状況・ルート最適化) APIが配送リクエストを受け取り、Schedulerを呼び出す
依存 ドメイン層の概念のみ ドメインサービス、リポジトリ、認証、通知等

ドメインイベント — 「何が起きたか」を表現する

ドメインイベントは、ドメインで発生した重要な出来事を表すオブジェクトです。 過去形で命名します(例: OrderPlacedDeliveryCancelled)。

ドメインイベントは集約の状態変更後に発行され、集約間の結果整合性を実現する主要な連携手段です。 「テーブルにレコードが挿入された」はドメインイベントではありません。 「配達がキャンセルされた」がドメインイベントです。ビジネスにとって意味のある出来事を表現します。

sequenceDiagram
  participant Client as クライアント
  participant Order as 注文集約
  participant Event as イベントバス
  participant Inventory as 在庫コンテキスト
  participant Notification as 通知コンテキスト

  Client->>Order: 注文を確定する
  Order->>Order: ビジネスルール検証
  Order->>Event: OrderConfirmed発行
  Event->>Inventory: 在庫を引き当てる
  Event->>Notification: 確認メールを送信する
ドメインイベントによる集約間の結果整合性: 注文確定イベントが在庫引き当てと通知を非同期にトリガーする

ファクトリ — 複雑なオブジェクト生成の隠蔽

ファクトリは、複雑なオブジェクト(特に集約ルート)の生成ロジックをカプセル化するパターンです。 オブジェクトが常に有効な状態で生成されることを保証します。 ただし、単純な生成にはコンストラクタで十分であり、ファクトリは複雑な場合のみ導入します。

理解度チェック

問題 0 / 40%
Q1

エンティティと値オブジェクトの最も根本的な違いは何ですか?

キーボード: 1〜4 で選択、Enter で回答