メインコンテンツまでスキップ

戦略的設計 — サブドメイン・ユビキタス言語・コンテキストマップ

前章で、DDD には「どこに線を引くか」と「線の内側をどう作るか」の 2 つの層があると書きました。本章は前者を扱います。

線を引く作業は 3 つに分かれます。事業のどこに力を入れるかを決める (サブドメイン)、チームで使う言葉を揃える (ユビキタス言語)、モデルが通用する範囲を区切る (境界づけられたコンテキスト)。最後に、区切った範囲どうしをどう繋ぐかを決めます (コンテキストマップ)。

ドメインを分ける — コア・支援・汎用

事業のすべての部分が同じ重みを持つわけではありません。DDD では、ドメインを性質の違うサブドメインに分けて考えます。

種類何か扱い方
コア競合との差がつく部分。事業の収益を生む中心自社で作る。設計に一番時間をかける
支援コアを回すのに要るが、差別化にはならない部分自社で作るが、コアほど作り込まない
汎用どの会社でも同じ形になる部分既製品や SaaS を買う。自作しない

EC サイトで考えます。

種類理由
コア注文・在庫引当在庫の押さえ方と確定のタイミングが事業の競争力に直結する
支援商品カタログ必要だが、作り込んでも他社との差はつきにくい
汎用認証・決済・メール配信Laravel Sanctum、Stripe、SES で足りる

この分類は、どこに設計の労力を割くかの判断材料です。汎用サブドメインに値オブジェクトと集約を丁寧に設計しても、事業への効果は小さいです。認証を自前のドメインモデルで組むより、第 22 章のように既存のパッケージへ寄せたほうが、コアに使える時間が増えます。

注記

コアかどうかは技術の複雑さでは決まりません。技術的に難しくても汎用のことがあり (全文検索)、単純でもコアのことがあります (値引きの適用順序)。判断の軸は「ここで差がつくか」です。

ユビキタス言語

ユビキタス言語とは、チーム全体で共通して使う言葉のことです。開発者、ドメインエキスパート (業務担当者)、プロダクトマネージャーなど、プロジェクトに関わるすべての人が同じ言葉を使うことで、誤解を防ぎます。

なぜ言葉を揃えるのか

言葉が揃っていないと、業務とコードの間に翻訳が挟まります。

言葉が揃っていない場合揃っている場合
業務担当者「注文を確定してください」「注文を確定してください」
開発者orders テーブルの status を 1 に更新します」Order::confirm() を呼びます」
起きること「確定」という概念がコードから消える業務の言葉がそのままコードに残る

翻訳が挟まると、仕様変更のときに毎回同じ翻訳をやり直すことになります。「確定のときに在庫を押さえる」という要件を聞いたとき、status を 1 にする処理のどこに在庫の処理を足すかを考えるのと、Order::confirm() の中を見るのとでは、探す手間が違います。

業務の言葉とメソッド名が対応していると、コードが仕様の説明になります。

業務の言葉コード
注文を確定するOrder::confirm()
注文をキャンセルするOrder::cancel()
商品を追加するOrder::addItem()
送料を計算するOrder::calculateShippingFee()
注記

ユビキタス言語は「業務用語辞書」ではありません。チーム全員が日常的に使う生きた言葉です。会議、コード、ドキュメントのすべてで同じ言葉を使い続ける必要があります。辞書を作って終わりにすると、辞書と実態がずれていきます。

境界づけられたコンテキスト

言葉を揃えようとすると、すぐに壁に当たります。同じ言葉が、場面によって違うものを指すからです。

Martin Fowler は電力会社の例を挙げています。「メーター」という語が、ある部署では接続地点を指し、別の部署では物理的な計器そのものを指す。会話では曖昧さが許容されますが、プログラムでは許容されません1

境界づけられたコンテキストとは、特定のドメインモデルが有効な範囲のことです。範囲を区切れば、同じ言葉でもコンテキストごとに別のモデルを持てます。

同じ「商品」が場面で変わる

EC サイトの「商品」を 3 つの場面で並べます。

コンテキスト「商品」が指すもの持つ情報
注文注文明細に含まれる購入対象商品 ID、商品名、価格、数量
在庫倉庫で管理される在庫品商品 ID、在庫数、倉庫ロケーション、入荷予定日
カタログ顧客に表示される商品情報商品 ID、商品説明、画像、カテゴリ

共通するのは商品 ID だけです。この 3 つを 1 つの Product クラスにまとめると、注文の処理で入荷予定日が見え、カタログの表示で在庫数が見える状態になります。どの属性がどの場面で意味を持つかが分からなくなり、変更のたびに無関係な場面を壊していないか確かめることになります。

境界を引くと、それぞれのコンテキストが自分に必要な情報だけを持てます。

部署の仕事の範囲に似ている

境界づけられたコンテキストは、部署ごとの仕事の範囲に似ています。

部門「顧客」が指すもの関心事
営業売上を生む取引先契約金額、取引履歴、担当者
サポートサポートを提供する相手問い合わせ履歴、製品情報、対応状況

同じ「顧客」でも、部署ごとに見たいものが違います。営業部門に問い合わせ履歴を見せても判断には使わず、サポート部門に契約金額を見せても対応は変わりません。

注記

小規模なシステムでは、すべてを 1 つのコンテキストで扱っても問題ありません。コンテキストの分離は、同じ語が違う意味を持ち始めてから検討すれば十分です。先に分けると、実態のない境界をまたぐ変換コードだけが残ります。

コンテキストマップ — 境界どうしの関係

境界を引いたら、境界どうしがどう繋がるかを決めます。コンテキストマップは、この関係を書き出したものです。

関係はまずチームの力関係で 3 つに分かれます2

関係の型内容
相互依存2 つのコンテキストが揃わないとどちらも動かない
上流・下流上流の変更が下流に影響するが、逆は影響しない
独立互いの変更が影響しない

この上に、具体的なパターンが乗ります。実務で判断が要るのは次の 5 つです。

パターンどういう関係か選ぶとき
パートナーシップ両チームが協調して一緒にリリースする片方が失敗すると両方が失敗する関係のとき
共有カーネルモデルの一部を明示的に共有する重複を避けたいが、変更のたびに両チームの合意が要る
顧客・供給者下流の要望を上流の計画に組み込む上流に交渉の余地があるとき
順応者上流のモデルにそのまま従う上流に交渉の余地がなく、変換の複雑さを避けたいとき
腐敗防止層上流のモデルを自分のモデルへ変換する層を挟む上流のモデルを自分の設計に入れたくないとき

腐敗防止層を厚く見る

5 つのうち、Laravel で最もよく使うのが腐敗防止層 (Anticorruption Layer、ACL) です。

外部サービスのモデルは、自分のドメインの都合で作られていません。Stripe の PaymentIntent は 7 つの状態を持ちます。requires_payment_method / requires_confirmation / requires_action / processing / requires_capture / canceled / succeeded です3。この 7 状態は決済フローの都合で分かれていて、注文の側から見ると「終わった」「まだ」「駄目だった」の 3 つで足ります。

7 状態をそのままドメイン層へ持ち込むと、注文のコードが決済サービスの事情を知ることになります。Stripe が状態を 1 つ足したら、注文の分岐も直すことになります。

腐敗防止層は、外部のモデルを自分の言葉へ翻訳する層です。

// app/Infrastructure/Payment/StripePaymentGateway.php

// ドメイン層が知っているのは PaymentResult だけ。
// PaymentIntent という語はこの層から外へ出さない。
final class StripePaymentGateway implements PaymentGatewayInterface
{
public function pay(OrderId $orderId, Money $amount): PaymentResult
{
$intent = $this->stripe->paymentIntents->create([
'amount' => $amount->amount(),
'currency' => strtolower($amount->currency()),
'metadata' => ['order_id' => $orderId->value()],
]);

// 外部の 7 状態を、自分のドメインが持つ 3 状態へ畳む。
// requires_* の違いは決済サービスの都合で、注文の側には
// 「まだ終わっていない」という 1 つの意味しか持たない。
return match ($intent->status) {
'succeeded' => PaymentResult::completed(new PaymentId($intent->id)),
'processing',
'requires_action',
'requires_capture',
'requires_confirmation',
'requires_payment_method' => PaymentResult::pending(new PaymentId($intent->id)),
'canceled' => PaymentResult::failed(
$intent->last_payment_error->message ?? '決済がキャンセルされました',
),
};
}
}

畳み込みの粒度を決めるのはドメインの側です。在庫の引当を決済の完了まで待つ設計なら、pendingcompleted の区別だけが要ります。逆に「カード認証の追加操作が要る」を画面に出したいなら、requires_action を独立した状態として持つ必要があります。外部の状態をいくつに畳むかは、自分の業務が区別する必要のある数で決まります。

この形にしておくと、決済サービスを差し替えるときに変わるのはこのクラスだけです。ドメイン層とユースケース層は PaymentGatewayInterfacePaymentResult しか知らないので、PaymentIntent という語が出てくる場所が 1 箇所に閉じます。

インターフェースをどの層に置くかは第 17 章のリポジトリと同じ判断になります。契約はドメイン層に置き、実装だけをインフラ層に置きます。

Laravel プロジェクトでの線の引き方

境界づけられたコンテキストは、必ずしも別のアプリケーションを意味しません。1 つの Laravel アプリケーションの中で、ディレクトリとして分けることから始められます。

app/Domain/
├── Order/ ← 注文コンテキスト
│ ├── Order.php
│ ├── OrderLine.php
│ └── OrderRepositoryInterface.php
├── Inventory/ ← 在庫コンテキスト
│ ├── Inventory.php
│ └── InventoryRepositoryInterface.php
└── Shared/ ← どのコンテキストでも同じ意味を持つもの
├── Money.php
└── EmailAddress.php

このとき守る約束は 1 つです。コンテキストをまたぐときは、相手の内部クラスを直接触らない。注文コンテキストが在庫の数量を変えたいなら、Inventory エンティティを直接操作するのではなく、識別子を渡してユースケース層で調整します。この理由は第 10 章の集約の話と同じで、整合性を誰が保証するかの問題です。

別のアプリケーションへ割るかどうかは、次の条件で判断します。

  • デプロイの単位を分けたいか。在庫だけを頻繁にリリースしたいなら分ける理由になります
  • チームが分かれているか。同じコードベースを触るチームが増えると、調整のコストが上がります
  • データの整合性を即時に保つ必要があるか。分けると 1 つのトランザクションで両方を更新できなくなり、結果整合性で設計することになります

最初から分けないほうが安全です。1 つのアプリケーションの中で境界を保てているなら、分けるのはいつでもできます。逆に、境界が曖昧なまま分けると、サービス間で密結合したまま通信の複雑さだけが増えます。

境界を引き間違えたときに出る症状

線の位置が違うことは、次の症状で気づけます。

症状何が起きているか
1 つのクラスの属性が、場面によって null になる複数のコンテキストを 1 つのモデルで表している
ある機能を変えると、無関係に見える機能のテストが落ちる境界をまたいで内部を直接触っている
同じ語をチーム内で言い換えている (「注文」「オーダー」「受注」)ユビキタス言語が揃っていない。背後に別のコンテキストがある
1 つの変更で複数のチームの合意が要る共有カーネルが大きすぎる

症状が出てから引き直せます。最初から正しい線を引く必要はありません。次章のワークショップは、業務の流れを並べて線の候補を見つける手法です。

まとめ

ポイント内容
サブドメインコア・支援・汎用に分け、コアに設計の労力を寄せる
ユビキタス言語業務の言葉とコードの言葉を揃える。辞書でなく日常的に使う言葉
境界づけられたコンテキストモデルが通用する範囲。同じ語が違う意味を持ったら分ける合図
コンテキストマップ境界どうしの関係。Laravel では腐敗防止層を使う場面が多い
Laravel での分け方まず app/Domain/<Context>/ のディレクトリで分ける。アプリを割るのは後

次章では、この境界をどう見つけるかを扱います。

参考資料

  • Martin Fowler「BoundedContext」 — 境界づけられたコンテキストの概要と、語の意味が場面で変わる例
  • ddd-crew/context-mapping — コンテキストマップの 9 パターンと、チーム間の関係の 3 分類
  • Eric Evans『エリック・エヴァンスのドメイン駆動設計』(翔泳社) 第 4 部 — 戦略的設計の原典
  • Vaughn Vernon『実践ドメイン駆動設計』(翔泳社) — サブドメインと境界づけられたコンテキストを実装寄りに扱う

Footnotes

  1. Martin Fowler「BoundedContext」

  2. パターン名と分類は ddd-crew/context-mapping の整理に沿っています。原典は Eric Evans『エリック・エヴァンスのドメイン駆動設計』第 4 部。

  3. Stripe API Reference: The PaymentIntent objectstatus フィールド。なお Charges API は新規の決済に使えません