戦術的設計の全体像
前章で学んだ戦略的設計がシステム全体の「大きな絵」を描くのに対し、 戦術的設計(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
エンティティ(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つのルールを提唱しています。
- 小さく設計する: 単一トランザクション内で一貫性が必要なデータのみ含める
- 他の集約はIDのみで参照する: 直接のオブジェクト参照を持たない
- 集約間は結果整合性を使う: ドメインイベントで非同期に連携する
- リポジトリは集約ルートに対してのみ定義する
リポジトリ(Repository) — 永続化の抽象化
リポジトリは、集約の永続化と取得を抽象化するインターフェースです。 ドメイン層からインフラストラクチャ(データベース等)の詳細を隠蔽し、集約をコレクションのように扱えるようにします。
リポジトリは集約ルートに対してのみ定義します。集約内部のエンティティへの個別リポジトリは不要です。
ドメインサービス — 複数の集約にまたがるロジック
ドメインサービスは、エンティティや値オブジェクトに自然に属さないビジネスロジックを担うステートレスなオブジェクトです。 複数のエンティティや集約にまたがるビジネスルールをカプセル化します。
| ドメインサービス | アプリケーションサービス | |
|---|---|---|
| 役割 | 複数集約にまたがるビジネスルール | ユースケースのオーケストレーション(調整役) |
| ビジネスロジック | 含む | 含まない |
| 例 | 配送スケジューリング(空き状況・ルート最適化) | APIが配送リクエストを受け取り、Schedulerを呼び出す |
| 依存 | ドメイン層の概念のみ | ドメインサービス、リポジトリ、認証、通知等 |
ドメインイベント — 「何が起きたか」を表現する
ドメインイベントは、ドメインで発生した重要な出来事を表すオブジェクトです。
過去形で命名します(例: OrderPlaced、DeliveryCancelled)。
ドメインイベントは集約の状態変更後に発行され、集約間の結果整合性を実現する主要な連携手段です。 「テーブルにレコードが挿入された」はドメインイベントではありません。 「配達がキャンセルされた」がドメインイベントです。ビジネスにとって意味のある出来事を表現します。
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: 確認メールを送信する
ファクトリ — 複雑なオブジェクト生成の隠蔽
ファクトリは、複雑なオブジェクト(特に集約ルート)の生成ロジックをカプセル化するパターンです。 オブジェクトが常に有効な状態で生成されることを保証します。 ただし、単純な生成にはコンストラクタで十分であり、ファクトリは複雑な場合のみ導入します。
理解度チェック
エンティティと値オブジェクトの最も根本的な違いは何ですか?
キーボード: 1〜4 で選択、Enter で回答