ドメイン駆動設計入門

Tommy Cheese | · 読了目安:9 分

ドメイン駆動設計の概要

ドメイン駆動設計(DDD、Domain-Driven Design)は、業務ドメインを中心にソフトウェアを設計する手法です。開発者とドメインエキスパートが業務知識を共に探究し、ドメインモデルを作り、その概念・ルール・言葉をコードに反映することで、複雑な業務の理解と保守を助けます。

DDD には、相互に影響する戦略的設計と戦術的設計があります。戦略的設計ではサブドメイン、境界づけられたコンテキスト、その協力関係を扱います。戦術的設計では各コンテキスト内のエンティティ、値オブジェクト、集約、ドメインサービス、ドメインイベントなどを扱います。両者は実践を通じて継続的に見直します。

ECの例:業務サブドメインにコンテキストを対応づけ、注文コンテキスト内に注文集約を置く。集約ルート自体はエンティティ。
これは設計の一例です。サブドメインとコンテキストの対応はモデリングで決めます。集約は特定のコンテキスト内に存在します。

戦略的設計の概念

ドメイン

ドメインとは、ソフトウェアが対象とする業務知識と活動の範囲です。EC システムなら、商品販売、決済、注文の履行などが関係します。ドメインを理解するには、機能を列挙するだけでなく、業務の目的・プロセス・ルールを話し合う必要があります。

サブドメイン

業務能力に基づいてサブドメインを識別し、競争優位への貢献と成熟した汎用的な解決策の有無に応じて分類します。

  • コアサブドメイン:企業の差別化と競争優位を生む業務能力で、モデリングや開発に重点的に投資する対象です。
  • 支援サブドメイン:業務運営に必要で企業固有の要件を持ちますが、直接的な競争優位の源泉ではない能力です。
  • 汎用サブドメイン:業界に成熟した解決策があり、通常は購入や再利用で対応できる能力です。

分類は企業の事業によって変わります。認証は一般的な EC 企業では汎用サブドメインでも、認証サービス提供企業ではコアになり得ます。複数のモジュールから使われることだけでは、汎用サブドメインとは判断できません。

ユビキタス言語

ドメインエキスパートと開発者は、同じコンテキスト内で共通の言葉を作り、議論・文書・コードで継続的に使います。例えば「支払い済みの注文」が全額入金を必要とするのか、いつキャンセルできるのかについて、共通理解が必要です。言葉の一貫性はコンテキスト内で保ち、企業全体に単一モデルを強制するものではありません。

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

境界づけられたコンテキスト(Bounded Context)は、特定のドメインモデルと言葉が一貫して有効な範囲を定めます。サブドメインは業務上の問題を分け、コンテキストはモデルの境界を定めます。両者を固定的な包含関係として定義してはいけません。明確な対応を目指しつつ、既存システムや組織の制約も考慮します。

同じ「商品」でも、販売コンテキストでは価格や割引を、倉庫コンテキストでは保管場所や入出庫ルールを重視するかもしれません。商品 ID で関連づけることはできますが、すべての属性と振る舞いを持つ単一の商品クラスを共有する必要はありません。

業務プロセス全体は複数のコンテキストをまたげます。注文・決済・履行の各コンテキストが協力して、注文受付から支払い、出荷までを実現できます。コンテキストマップで関係と連携契約を明確にし、必要なら腐敗防止層で外部モデルを変換して自分のモデルを守ります。

コンテキストはサービス境界を考える重要な手掛かりですが、デプロイ単位と同じではありません。DDD はモジュール化されたモノリスにもマイクロサービスにも適用できます。ひとつのコンテキストを複数のモジュールや物理サービスで実装することもあり、「ひとつのコンテキストにひとつのマイクロサービス」は必須ルールではありません。

戦術的設計の概念

値オブジェクトとエンティティ

エンティティと値オブジェクトを区別する基準は、業務上、その同一性を継続して追跡する必要があるかどうかです。

  • エンティティ(Entity):同一性で識別するドメインオブジェクトです。注文金額や状態が変わっても、同じ注文 ID なら同じ注文を表します。ID の異なる注文は、金額が同じでも別のエンティティです。
  • 値オブジェクト(Value Object):追跡すべき独立した同一性を持たず、業務上意味のある値で比較します。金額と通貨が同じ金額オブジェクトは交換可能ですが、通貨が違えば等しくありません。

値オブジェクトは通常、不変に設計し、変更時には新しい値で置き換えます。これにより共有参照を通じた意図しない変更を避けられます。エンティティを不変に実装することもできます。等価性の基準は同一性か値かであり、変更可能性ではありません。同じ概念でもコンテキストによってモデル化の仕方は変わります。

複雑な業務では、エンティティ、値オブジェクト、集約ルートに、それぞれに適した業務ルールをカプセル化します。注文のキャンセル処理で可否を確認すれば、呼び出し側が自由に状態を書き換えることを防げます。アプリケーション層はユースケースを組み立て、データベースアクセスなどの技術詳細はインフラストラクチャが担います。豊かなドメインの振る舞いを持つために、エンティティが直接データベースを呼ぶ必要はありません。

データと getter/setter だけを持ち、業務ルールをすべて外に置く設計は、一般に貧血ドメインモデルの特徴です。複雑なドメインで普遍的に推奨する設計ではありませんが、単純な CRUD なら簡素なデータモデルが適する場合もあります。POJO は普通の Java オブジェクトを意味し、業務ロジックの有無を制限しません。

リポジトリ Repository

リポジトリはドメインオブジェクトのコレクションに似たアクセスを提供し、クエリ、マッピング、保存方法の詳細を隠します。DDD では通常、永続化して直接アクセスする集約ルートごとに設計し、すべてのテーブルや内部エンティティごとに作るものではありません。

インターフェースは、注文 ID による読み込みや注文集約の保存など、明確なドメイン上の意図を表すべきです。メソッド名の短さは設計基準ではありません。データベースのフィールドや任意の単一テーブル更新をドメイン層に直接公開することも避けます。

MyBatis でリポジトリを実装できます。Mapper をそのままリポジトリのインターフェースとして使えるかは、集約の読み込み・保存・カプセル化の要件を満たすかによります。普通の単一テーブル CRUD Mapper は、保存操作を提供するだけで DDD のリポジトリになるわけではありません。

集約と集約ルート

集約(Aggregate)は、業務の不変条件を守るトランザクション整合性の境界です。不変条件とは、業務更新のコミット時に必ず成立するべきルールです。集約には複数のエンティティと値オブジェクトを含められますが、ルートエンティティだけの場合もあります。集約ルート自体はエンティティで、アクセスの入口となり、内部のルールを守ります。

ある注文モデルで、合計金額が明細金額の合計と常に一致する必要があるなら、注文と明細を同じ集約にし、注文ルートが明細変更を管理できます。境界を決めるのは整合性要件であり、関連するすべてのオブジェクトをまとめるという発想ではありません。

  • 外部コードから内部状態を変更するときは集約ルートの振る舞いを通します。内部エンティティを直接変更して業務ルールを迂回しないようにします。不変な値オブジェクトは安全に返せます。
  • 業務更新をコミットするとき、明細数量が正であることや合計金額が正しく計算されていることなど、即時に成立すべきルールを満たす必要があります。永続化には適切なトランザクションと並行性制御も必要です。
  • 集約間の参照は通常 ID を優先します。協力にはドメインイベントと結果整合性を使えます。より強い整合性が必要かは業務要件で判断し、関連があるだけで全オブジェクトをひとつにまとめません。

「読み込み・変更・保存」はユースケースを説明できますが、すべての読み込みを処理全体にわたるデータベーストランザクションに入れることを意味しません。集約境界は整合性要件を表し、具体的なトランザクションや並行性の戦略は実装時に決めます。

ドメインイベント Domain Events

ドメインイベントは、ドメイン内で既に起きた、業務上意味のある事実を表します。例えば「注文がキャンセルされた」です。業務の意味とルールに基づいて識別し、エンティティの任意の属性変更から自動的に生じるものではありません。名前には過去形を使うことが多く、記録する事実は通常不変です。

コンテキスト内部のイベントは、プロセス内で同期的にも非同期的にも配信できます。コンテキストをまたぐ場合は、通常、コミットに成功した業務上の事実を統合イベントに変換し、メッセージ基盤で他サービスへ送ります。メッセージキューは実装の選択肢であり、ドメインイベントの定義の一部ではありません。

例えば注文キャンセル後、在庫コンテキストに予約在庫の解放を知らせられます。「キャンセルが起きた」という記録を起きなかったことには書き換えられませんが、その後の業務処理や補償は可能です。イベントの不変性を、すべての業務結果が調整不能という意味に解釈してはいけません。

ドメインサービス Domain Service

重要なドメイン操作が、どのエンティティや値オブジェクトにも自然に属さない場合は、ドメインサービスとしてモデル化できます。例えば配送先、配送方法、運送会社の見積もりを組み合わせるモデルでは、送料計算をサービスに任せられます。判断基準は振る舞いの帰属であり、関係するオブジェクト数ではありません。

ドメインサービスは通常ステートレスで、サービスや操作はユビキタス言語で命名します。「注文に明細を追加する」が注文集約に自然に属するなら、注文の振る舞いとして実装し、状態同期用のサービスを追加する必要はありません。

アプリケーションサービスはユースケースの要求を受け取り、集約を読み込み、ドメインの振る舞いを呼び、保存やトランザクションを調整します。ドメインサービスは他のドメインオブジェクトに属さない業務ルールを担います。どちらも独立してデプロイするマイクロサービスと同義ではありません。

DDD のドメインモデリング

次の順序を出発点にできます。新しい知識や実装からのフィードバックを得たら、前のステップにも戻ります。

  1. ドメインエキスパートと典型的な業務シナリオ、ルール、目的を話し合い、ユビキタス言語と初期のサブドメインを作ります。
  2. モデルの一貫性が保たれる範囲を定め、コンテキストを分け、協力関係と連携契約を記述します。
  3. 各コンテキスト内でエンティティ、値オブジェクト、不変条件を識別し、それを基に集約境界、ルート、振る舞いを設計します。
  4. 必要に応じてドメインサービス、イベント、リポジトリ、オブジェクト生成を設計し、アプリケーション層とインフラストラクチャの責務を明確にします。
  5. 具体的なシナリオでモデルを検証し、重要なルールを確認します。業務理解が変わったら、コード・言葉・モデル境界を共に修正します。

初期の戦略的な区分を作ってから戦術モデルを深める方法は実用的ですが、後戻りできない二段階ではありません。DDD の重点は、継続的な協力とフィードバックでソフトウェアモデルに業務を正確に表現することです。業務の複雑さに応じてパターンを選び、概念を揃えるためだけに構造を増やさないようにします。

参考資料

コメント

GitHub アカウントでご参加ください。すべての言語版で同じコメント欄を共有しています。

ホーム・ニュース・最先端研究などでページに移動、または記事を検索します。

↑↓ 選択 ↵ 開く Esc 閉じる