LESSON 40分

ストーリー

高橋アーキテクト
ヘキサゴナルもクリーンアーキテクチャも、システムの”構造”を整理する方法だった
高橋アーキテクト
では、その構造の中心にある”ドメイン”をどう設計するか? それがドメイン駆動設計 — DDDだ
高橋アーキテクト
DDDは難しいと聞きました…
高橋アーキテクト
確かに奥が深い。でも、2つに分けて考えれば整理しやすい。大きな視点の”戦略的設計”と、コードレベルの”戦術的設計”だ。まずは戦略的設計から始めよう

DDDとは

ドメイン駆動設計(Domain-Driven Design)は、Eric Evansが2003年に提唱した、複雑なビジネスドメインをソフトウェアに正確に反映するための設計手法です。

DDD
├── 戦略的設計(Strategic Design)
│   ├── Bounded Context(境界づけられたコンテキスト)
│   ├── Ubiquitous Language(ユビキタス言語)
│   └── Context Map(コンテキストマップ)
└── 戦術的設計(Tactical Design)
    ├── Entity
    ├── Value Object
    ├── Aggregate
    ├── Domain Event
    ├── Repository
    └── Domain Service

Ubiquitous Language(ユビキタス言語)

ユビキタス言語とは、開発者とドメインエキスパート(業務担当者)が共通で使う言葉です。

なぜ必要なのか

ドメインエキスパート: 「この注文を"出荷停止"にして」
開発者:              「statusをCANCELLEDに更新ですね」
ドメインエキスパート: 「いや、出荷停止とキャンセルは違う。
                       出荷停止は一時的に止めるだけ。
                       キャンセルは注文そのものを取り消す」

→ 言葉のズレが、コードのバグになる

コードに反映する

// NG: 開発者の言葉(ドメインの概念が失われている)
class Order {
  updateStatus(status: string): void {
    this.status = status;
  }
}

// OK: ユビキタス言語(ドメインエキスパートの言葉がそのままコード)
class Order {
  // 「出荷を停止する」
  holdShipment(): void {
    if (this._status !== OrderStatus.CONFIRMED) {
      throw new Error('確認済みの注文のみ出荷停止できます');
    }
    this._status = OrderStatus.HELD;
  }

  // 「注文をキャンセルする」
  cancel(): void {
    if (this._status === OrderStatus.SHIPPED) {
      throw new Error('出荷済みの注文はキャンセルできません');
    }
    this._status = OrderStatus.CANCELLED;
  }

  // 「出荷を再開する」
  resumeShipment(): void {
    if (this._status !== OrderStatus.HELD) {
      throw new Error('出荷停止中の注文のみ再開できます');
    }
    this._status = OrderStatus.CONFIRMED;
  }
}

Bounded Context(境界づけられたコンテキスト)

Bounded Contextとは、特定のドメインモデルが有効な境界です。同じ言葉でも、コンテキストによって意味が異なります。

例: ECサイトの「商品」

┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐
│  カタログBC       │  │  注文BC          │  │  在庫BC          │
│                  │  │                  │  │                  │
│  Product         │  │  OrderItem       │  │  StockItem       │
│  - name          │  │  - productId     │  │  - productId     │
│  - description   │  │  - quantity      │  │  - quantity      │
│  - price         │  │  - unitPrice     │  │  - location      │
│  - images        │  │  - subtotal      │  │  - reorderLevel  │
│  - category      │  │                  │  │                  │
│                  │  │  ※価格は注文時に │  │  ※物理的な在庫   │
│  ※表示用の情報   │  │  確定した金額    │  │  管理の情報       │
└─────────────────┘  └─────────────────┘  └─────────────────┘

同じ「商品」でもコンテキストごとに異なるモデルを持つ
// カタログBC: 表示用の情報に焦点
// catalog/domain/Product.ts
class Product {
  constructor(
    readonly id: ProductId,
    private _name: string,
    private _description: string,
    private _price: Money,
    private _images: ProductImage[],
    private _category: Category
  ) {}

  updatePrice(newPrice: Money): void { /* ... */ }
  addImage(image: ProductImage): void { /* ... */ }
}

// 注文BC: 注文時の金額に焦点
// ordering/domain/OrderItem.ts
class OrderItem {
  constructor(
    readonly productId: string,  // カタログBCのProductIdを参照
    private _quantity: number,
    private _unitPrice: Money    // 注文時に確定した金額
  ) {}

  get subtotal(): Money {
    return this._unitPrice.multiply(this._quantity);
  }
}

// 在庫BC: 物理的な在庫管理に焦点
// inventory/domain/StockItem.ts
class StockItem {
  constructor(
    readonly productId: string,
    private _quantity: number,
    private _location: WarehouseLocation,
    private _reorderLevel: number
  ) {}

  needsReorder(): boolean {
    return this._quantity <= this._reorderLevel;
  }
}

Bounded Contextの特定方法

ステップ1: ドメインエキスパートとの対話

「注文管理について教えてください」
→ 注文の作成、確認、出荷停止、キャンセル、返品

「在庫管理はどう違いますか?」
→ 入荷、出荷引当、棚卸し、ロケーション管理

「カタログの商品情報と在庫の商品情報は同じですか?」
→ いいえ。カタログは顧客向けの情報、在庫は倉庫の物理的な情報です

→ 3つのBounded Context: カタログ、注文、在庫

ステップ2: 言語の境界を見つける

同じ言葉が異なる意味を持つ箇所が、Bounded Contextの境界です。

言葉カタログBC注文BC在庫BC
商品表示情報注文明細在庫品目
価格定価注文時確定価格仕入原価
数量なし注文数在庫数

Bounded Context間の連携

異なるBounded Context間では、ドメインモデルを直接共有しません。IDで参照するか、イベントで連携します。

// 注文BCから在庫BCへの連携
// NG: 直接参照(境界を越えてモデルを共有)
class CreateOrderUseCase {
  constructor(private stockItemRepo: StockItemRepository) {} // 在庫BCのリポジトリを直接使用
}

// OK: ドメインイベントで疎結合に連携
class CreateOrderUseCase {
  constructor(
    private orderRepo: OrderRepository,
    private eventPublisher: DomainEventPublisher
  ) {}

  async execute(input: CreateOrderInput): Promise<CreateOrderOutput> {
    const order = Order.create(input.customerId, input.items);
    await this.orderRepo.save(order);

    // イベントを発行して在庫BCに通知
    await this.eventPublisher.publish(
      new OrderCreatedEvent(order.id, order.items)
    );

    return { orderId: order.id.value };
  }
}

まとめ

ポイント内容
DDD複雑なドメインをソフトウェアに正確に反映する設計手法
ユビキタス言語開発者とドメインエキスパートの共通言語
Bounded Context特定のドメインモデルが有効な境界
連携方法IDで参照、またはドメインイベントで疎結合に
言語の境界同じ言葉が異なる意味を持つ箇所がBCの境界

チェックリスト

  • ユビキタス言語の重要性を説明できる
  • Bounded Contextの概念を説明できる
  • 同じ「商品」がコンテキストごとに異なるモデルを持つことを理解した
  • Bounded Context間の連携方法(ID参照、イベント)を理解した

次のステップへ

次は「DDDの戦術的設計」を学びます。Entity、Value Object、Aggregateなど、コードレベルのDDDパターンを身につけましょう。


推定読了時間: 40分