ドメイン駆動設計の概要
ドメイン駆動設計(DDD, Domain-Driven Design)は、モデルを中心とした設計手法です。ドメインモデルでドメインの知識を捉え、そのモデルを使って保守しやすいソフトウェアを構築します。
DDDの設計は、戦略的設計と戦術的設計に分かれます。戦略的設計ではドメイン、サブドメイン、境界づけられたコンテキストを設計し、戦術的設計ではエンティティ、値オブジェクト、ドメインイベントなどを設計します。その関係は次のとおりです:

戦略的設計に関する概念
ドメイン
ドメインとは、システムが解決する問題の領域です。たとえば、商品情報管理はシステムが扱うドメインの一つになります。
サブドメイン
使われる言葉の違いに基づき、ドメインを次のようなサブドメインに分けられます:
- コアドメイン:製品の競争力を決めるサブドメインです。最も重要で、業務の中核にあり、独自性を持つ部分です。
- 汎用サブドメイン:複数のサブドメインで使う共通機能です。認証や権限管理などの汎用システムが例です。企業固有の事情に左右されにくく、大幅なカスタマイズは必要ありません。
- 支援サブドメイン:コア機能でも汎用機能でもないサブドメインです。企業固有の特徴はありますが、汎用性はありません。コード値を扱うデータディクショナリなどが例です。
境界づけられたコンテキスト
境界づけられたコンテキストとは、異なる問題を異なるモデルで解決するために区切った、それぞれの範囲です。
境界づけられたコンテキストは、一つまたは複数のサブドメインの集合です。一つのコンテキストで、完結した業務フローを支えられるようにします。また、その業務フローに関係するドメインを、一つのコンテキストに収めます。境界づけられたコンテキストはマイクロサービスを分割する基準となり、各コンテキストが一つのマイクロサービスに対応します。
では、なぜコンテキストを区切る必要があるのでしょうか。それは、文脈の存在がコーディングを難しくするからです。例を挙げてみましょう:
採用のドメインでは「プラットフォーム」という言葉で、出身の学校や組織を表すことがあります。飛び込み競技では、選手が飛び込む台を指します。鉄道輸送のドメインでは、駅のホームを表すかもしれません。ある日、飛び込み選手、人事担当者、乗務員が偶然集まり、お互いの職業を知らないまま、人事担当者が選手に「どのプラットフォームの出身ですか」と尋ねたら……。
同じ「プラットフォーム」という言葉でも、三人の間に取り決めがなければ、理解が異なる可能性があります。この問題を解決するには、飛び込み、採用、交通の各ドメインを分離し、それぞれで用語を定義します。一つのドメイン内で話せば、曖昧さをできる限り避けられます。これが取り決めです。業務に関係するドメインを一つのコンテキストに定義するのがよい理由も、曖昧さを減らすためです。
ソフトウェアシステムの設計でも、同じような問題が起こります。飛び込み、採用、乗車の各サービスを含む大きな製品を設計しているとします。サービスを一切分割せず、飛び込みの業務ではplatformというデータ構造を飛び込み台に、採用では組織に、交通では駅のホームに使ったら、三つの業務が交わるときに問題が起きます。意味は違うのにほぼ同じ名前の変数が画面いっぱいに並んだら、どうすればよいでしょうか。
あるドメインの用語の意味を明確に限定すれば、そのドメイン内では迷わずに用語を使えます。これが境界づけられたコンテキストの重要な役割です。
戦術的設計に関する概念
値オブジェクトとエンティティ
まず、値オブジェクトとエンティティを理解しましょう。
- 値オブジェクトは、属性の値によって識別されるオブジェクトです。二つの値オブジェクトの内部の値がすべて同じなら、同じ値オブジェクトだとみなします。値オブジェクトの属性は不変です。
- エンティティは、一意な識別子と状態を持ち、ライフサイクルのある業務オブジェクトです。属性は変更できます。二つのエンティティの属性が完全に同じでも、それだけでは同じエンティティとはみなしません。IDなどの識別子が同じ場合にだけ、同じエンティティだとみなします。
例で値オブジェクトとエンティティを区別しましょう:
白色を表す値オブジェクトColor white{R:255, G:255, B:255}と、タイヤのエンティティtire{Air:,Size:}があるとします:
- 可変性:白色の各属性は不変です。一つでも変えると、そのオブジェクトはもう「白色」を表さなくなるからです。一方、タイヤの空気圧やサイズは変えられます。これらの値が変わっても、タイヤであることには変わりません。
- 比較可能性:値オブジェクトは値が不変だからこそ、等価性の規則を持ちます。二つの白色オブジェクトの値は必ず同じなので、内部の値がすべて同じなら、同じ値オブジェクトだとみなせます。エンティティは属性が可変なので、この性質を持ちません。
エンティティの形には、一般に四つあります:
- 失血モデル:データの定義とgetter/setterメソッドだけを持ち、業務ロジックとアプリケーションロジックはサービス層に置きます。このようなクラスはJavaではPOJOと呼ばれます。
- 貧血モデル:一部の業務ロジックを持ちますが、永続化層に依存する業務ロジックは含みません。その部分はサービス層に置きます。
- 充血モデル:永続化層に依存するものも含め、すべての業務ロジックを持ちます。
- 膨張モデル:認可やトランザクションなど、業務ロジックに直接関係しないアプリケーションロジックまで、すべてドメインモデルに置きます。
リポジトリ Repo
Repoは、Entityのために設計する保存操作です。Repoの操作はできるだけ低レベルにし、短い名前を付け、数を増やしすぎないようにします。JavaのMyBatis Mapperは、Repoの実装の一種とみなせます。
集約と集約ルート
集約は、より大きな範囲のカプセル化です。同じライフサイクルを持ち、業務上切り離せないエンティティと値オブジェクトをまとめて扱います。外部に参照を公開できるのは集約ルートだけであり、集約は凝集性の表れでもあります。集約ルート同士が呼び合うこともできます。集約ルートを抽象化した名前は、通常は名詞になります。
集約ルートは、次の方法でカプセル化を実現します:
- 集約全体への操作は、必ず集約ルートを通して行い、外部から内部の要素を直接操作することは許しません。たとえば、紫色の外観+タイヤ+スチールフレーム+……=自動車と考えます。車を運転するとき、タイヤ、ハンドル、外観などを個別の独立した要素として操作するのではなく、自動車という集約ルートを操作し、他のエンティティや値オブジェクトを間接的に扱います。
- 集約は一組の境界を定義し、その内側のすべての要素が、業務ロジックに照らして有効である必要があります。
- 集約は、一つの原子的なトランザクション内で操作しなければなりません。そうでないと不整合が起こり得ます。集約が操作の単位であり、リポジトリから取り出し、操作を終えて戻すまでが一つの原子的な操作です。原子性とは何でしょうか。自動車の集約ルートに車輪やスチール/アルミ/カーボンのフレームのエンティティがあるとして、車輪を一つ外して点検した後、そのまま取り付けずに終えることはできない、ということです。
ドメインイベント Domain Events
エンティティの属性が変わると、ドメインイベントが発生します。ドメインイベントとは、ドメインの専門家が重要だと考える出来事です。ドメイン内で起きた、注目すべきイベントであり、通常はドメインオブジェクトの状態変化を意味します。ドメインイベントは、システム内でメッセージを伝え、他のアクションを起動する役割を持ち、ドメインモデルを疎結合にする重要な手段の一つです。イベントの伝達には、よくメッセージキューを使います。そのメッセージを購読するすべてのサブドメインが、それぞれ内部で応答処理を行います。
メッセージバスも実装方法の一つです👋。
たとえば、タイヤの空気圧が変化すると、空気漏れを表すleakedドメインイベントを発行します。このイベントが「タイヤの空気が漏れた」と伝えることで、駆動系の変化やハンドルの感触の変化など、一連の反応が起きます。ドメインイベントには通常、すでに発生して取り消せない出来事だとわかるよう、過去形の名前を付けます。
ドメインサービス Domain Service
ドメイン内には、どのオブジェクトにも属さないように見える操作があります。それはドメインの重要な振る舞いであり、無視したり、単純にいずれかのエンティティや値オブジェクトへ押し込んだりはできません。このような振る舞いを見つけたら、サービスとして宣言する方法が推奨されます。それがドメインサービスです。
ドメインサービスは、マイクロサービスの「サービス」とは別の概念です。サービスと集約ルートは、どちらも複数のエンティティを操作できる点では似ていますが、考え方と役割が異なります。集約ルートはエンティティをまとめるものであり、サービスは複数のエンティティの状態を同期するものです。たとえば、itemエンティティをlistエンティティへ追加します。mailをinboxへ配信する操作などが例です。そのため、Serviceを抽象化した名前は通常、動詞になります。
DDDのドメインモデリング(ドメインモデルの設計)
DDDのドメインモデリングは、一般に次の手順で進めます:
- 要件に基づいて、暫定的なサブドメインと境界づけられたコンテキスト、およびコンテキスト間の関係を決めます。
- 各コンテキストの内部をさらに分析し、エンティティと値オブジェクトを見分けます。また、エンティティに失血、貧血、充血、膨張のどのコードモデルを使うかを決めます。
- エンティティと値オブジェクトを関連付けて集約し、集約の範囲と集約ルートを決めます。
- 集約ルートのためのrepoを設計し、エンティティや値オブジェクトの生成方法を考えます。
- プロジェクトでドメインモデルを実装し、実践を通じて妥当性を検証します。不十分な点をモデルへフィードバックして、リファクタリングします。
これが、最初に戦略的設計を行い、その後で戦術的設計を行う、DDDの二段階の設計です。
実践では、エンティティに失血モデル(setter/getterだけを持つ)か、貧血モデル(属性の妥当性検証など、データベース操作を含まない簡単なロジックを持つ)を採用することを勧めます。
実際には、これはDDDモデリングの方法の一つで、トップダウンの設計です。別の方法としてボトムアップもあり、先にエンティティや値オブジェクトなどのドメインモデルを定め、その後でサブドメインや境界づけられたコンテキストを決めます。
(本節・完)