ドメイン駆動設計の中でも具体的なレベルで書かれている構成要素についてまとめます。
ここで指している構成要素とはエリック・エヴァンスのドメイン駆動設計の最初のページにある図の下半分に書かれているものです。
※図は下記ブログ記事の2枚目を指しています。
ドメイン駆動設計の源流のPofEAAを読んでみる
そもそもドメイン駆動設計とは
ドメインとは
ドメインはプログラムを適用する対象領域を指します。
例えば宅配便をドメインとする場合、配送業者・顧客・配達物などがドメインに含まれます。
ドメイン駆動設計とは
ドメインの問題を解決するためのモデルを元にソフトウェアを設計する手法です。
ここでのモデルとは、ドメインの問題を解決するために現実のモノ・コトを抽象化した概念を意味します。
例として、宅配便における配達をモデルとする場合、以下のような情報が必要です。
- 配達番号
- 配達先住所
- 配達先氏名
このとき現実のモノ・コトをすべてモデルに反映する必要はありません。
例えば、配達時の車のタイヤの種類について、現実には存在していますが宅配便の配達業務への影響はありません。そのため、ドメインの問題解決には不要な情報といえます。
上記のようなモデルを考えるために現実のモノ・コトを抽象化する作業をモデリングと呼びます。
そして作成したモデルを元に具体的な実装を進めていきます。
モデルがあると何が嬉しいのか
-
保守や継続的な開発がしやすくなる
コードはモデルを元に開発されます。つまりモデルを理解することで、コードが何をしているか把握することができます。そのため、モデルがあることでソフトウェアの保守・継続的な開発がしやすくなります。 -
ビジネスチームとコミュニケーションが取りやすくなる
モデルはエンジニアではなくても理解できる言語・形式のため、機能や仕様を考える企画部門などビジネスチームも利用できます。モデルという共通言語を利用することで認識の齟齬が発生しにくく、コミュニケーションが取りやすくなります。 -
ドメイン知識が蒸留されていく
継続的にモデリングを繰り返すことで、ドメインの中の必要な要素だけがモデルに抽出され不要な要素は削除されていきます。そのため、モデルにはドメインの問題解決に必要な知識が蒸留されていきます。蒸留の意味は、「混合物を一度蒸発させ、後で再び凝縮させることで、沸点の異なる成分を分離・濃縮する操作」で、DDDにおいてはドメインの問題解決に必要なものと不要なものを分ける操作といえます。
ユビキタス言語
ユビキタス言語とは、ドメインモデルに関するコミュニケーション(会話やドキュメント)とコードで使用する共通言語です。※ユビキタスは「どこでも (everywhere)」を意味します。
開発者とビジネスチームで異なる言語を使用している場合、一度自分たちの分かる言葉に変換する手間や認識の齟齬が発生しやすくなります。
少し極端な例ですが、管理者のみが使える機能があった場合、会話では「管理者」、ドキュメントでは「マネージャー」、コードでは「Admin」となっていると、会話・ドキュメント・コード間で認識のずれが発生しやすくなります。
ドメインモデルに関することはユビキタス言語を使うことで、言葉の変換の手間や認識のずれを防止することができます。
モデル駆動設計
モデル駆動設計は、モデルと実装を結びつける設計手法です。
もしモデルをドメインの理解・分析用(業務従事者や企画者向け)とソフトウェアの設計用(エンジニア向け)などに分割してしまった場合、ドメインに詳しい人の考えや意図が設計に正確に伝わらず、最終的にできるコードにも反映されなくなってしまいます。
モデル駆動設計では、1つのモデルだけでドメイン知識の習得やソフトウェア設計ができるようにモデルを作成します。1つのモデルにすることでドメインの知識が正確に設計・コードに反映させることができます。つまり、モデルと実装を結びつけることで先述した問題を起こす可能性を低くすることができます。
このようなモデルを作成するためには、モデル・設計・コードを一体として捉え、繰り返しドメイン知識の抽象化とリファクタリングが必要とされています。
レイヤ化アーキテクチャ
レイヤ化アーキテクチャとは、ソフトウェアを各レイヤーに分割することで、ドメインモデルに表現されている知識やルールをコードに反映させやすくするためのアーキテクチャです。
ドメイン駆動設計では、ドメインに関するルールや制約に関するコードは1箇所にまとめるべきとしています。もしドメインに関するコードが複数の場所に拡散していると仕様の把握や修正にも時間が掛かってしまいます。
例えば、ユーザー登録の機能で「ユーザー名は10文字以内」というドメインのルールが画面側に書かれてしまった場合、更新画面にも同じ処理を書く必要があります。もし「ユーザー名は10文字以内」という仕様が変更された場合、登録・更新画面どちらかの変更が漏れてしまう恐れもあります。
上記の問題を回避するためのアーキテクチャとして、エリック・エヴァンスのドメイン駆動設計で挙げられているのがレイヤー化アーキテクチャです。
層 | 概要 |
---|---|
プレゼンテーション層 | ユ-ザーからの入力、ユーザーへの出力 |
アプリケーション層 | ドメイン層を呼び出しソフトウェアのユースケースを実現する |
ドメイン層 | ドメインに関する知識やルール |
インフラストラクチャ層 | 上位レイヤーを実現するための技術的な機能(メッセージ送信やDBとの入出力など) |
レイヤー化アーキテクチャで、役割別にコードを分割することで各コードの仕様の把握や修正がしやすくなります。特にドメイン層を隔離することで、ドメイン層にはモデルに表現しているドメインの知識やルールに関するコードのみが書かれるため、モデルとコードを紐付けさせやすくなります。
エンティティ
エンティティとは同一性と連続性の性質を持つオブジェクトです。
同一性は、他のものの中から一意に識別することができる性質です。
連続性は、特定の期間で状態が変化するという、ライフサイクルを持つ性質です。
ここでは宅配便における荷物を例に説明します。
荷物は他の荷物と区別するためや配達業者が配達状況を追跡するため、一意に識別する際に必要があります。そのため、荷物には一意に識別するための配達番号が付与されます。
つまり、荷物には他の荷物と区別するための同一性・配達状況が変化する連続性をもっているためエンティティとして分類されます。
エンティティは同一性と連続性により、データを特定・追跡することができます。
値オブジェクト
値オブジェクトとは、同一性を持たず、不変性の性質を持つオブジェクトです。
不変性とは、オブジェクトの値が変更されず同じ値を保持する性質です。
エンティティと同じく宅配便における配達日時を例に説明します。
配達日時オブジェクトは日時情報だけを持つオブジェクトで他の配達日時オブジェクトと区別することはないため、配達番号のような一意の識別子は不要です。
また値が変わらないという不変性についてですが、宅配便で配達日時を変更するケースはあるかと思います。その場合、古い値を破棄して新しい値オブジェクトを生成します。一方でエンティティである荷物はその属性値である配達日時を変更しても、新しい荷物になるわけではなく変更前と同じ荷物であることに変わりありません。
値オブジェクトはドメインで扱う文字列・数値・日時といった値をもつオブジェクトとして表現します。
サービス
サービスはドメインにおける活動や行動を表現するオブジェクトです。
宅配便のドメインにおいて、配達時に配達ステータスの更新と注文者に配達結果を通知するルールがあるとします。
このドメインルールでは以下のエンティティ(E)・値オブジェクト(V)を参照・操作することでドメインルールを表現しています。
- 配達(E)の中の配達ステータス(V)を更新する
- 注文者(E)の中の氏名(V)と配達(E)の中の配達番号(V)・配達ステータス(V)から通知メッセージの文面を作成する
上記のような、ドメインの単一の知識であるエンティティ・値オブジェクトだけでは表現ができず、複数のエンティティ・値オブジェクトを組み合わせてドメインルールを表現するサービスをドメインサービスと呼びます。
ドメインサービス以外のサービス
ドメインの知識を表現するドメインサービス以外にも下記サービスがあります。
ただしドメインに関するルールはあくまでドメインサービスにのみ記載します。
インフラストラクチャサービス
インフラストラクチャサービスは、ドメインの知識やルールには直接関係しない技術的な機能を提供するサービスです。
先程の例である「配達時のメッセージ送信」では、ドメインサービスが作成した通知メッセージの送信を担当します。
アプリケーションサービス
アプリケーションサービスは、アプリケーションのユースケースを実現するサービスです。
先程の例である「配達時のメッセージ送信」というユースケースを実現するために下記を担当します。
- リポジトリ(後述)を呼び出し配達エンティティ・注文者エンティティを取得する
- ドメインサービスに配達エンティティ・注文者エンティティを渡す
- ドメインサービスで作成された通知メッセージをインフラストラクチャサービスに渡す
モジュール
モジュールとは、ドメインの中の関連性のあるクラスやファイルをまとめたものです。
これはディレクトリ内のフォルダを分けることで表現します。
関連性のあるクラスやファイルがまとまっていることで、ドメインの影響箇所の特定がしやすくなり、モジュール間の関係性も把握しやすくなります。
宅配便の例として、下記のようにdeliveryと大きな括りでまとめてしまうと、顧客(customer)や請求(billing)など他ドメインの要素も混在するため、顧客ドメインに関するコードを調べたいときなど、特定のドメインの要素を探したいときに関連性のある要素を探しづらくなっていまいます。
src
└─delivery
└─domain
├─entity
│ Customer.cs
│ Delivery.cs
│ Billing.cs
│
├─service
└─valueObject
大きな括りではなく、関連性の高いドメインごとにディレクトリを整理することで、各ドメイン内の要素を探しやすくなります。
src
├─billing
│ └─domain
│ ├─entity
│ │ Billing.cs
│ │
│ ├─service
│ └─valueObject
├─customer
│ └─domain
│ ├─entity
│ │ Customer.cs
│ │
│ ├─service
│ └─valueObject
└─delivery
└─domain
├─entity
│ Delivery.cs
│
├─service
└─valueObject
集約
集約は、ドメイン内で整合性が必要なエンティティ・値オブジェクトのまとまりです。
集約のルールとして、下記2点があります。
①集約ルートによるアクセス制御
整合性が必要なオブジェクト群を集約としてまとめる時、集約のルートとなるオブジェクトを1つ選択します。
集約外部のオブジェクトが直接参照できるのは、集約ルートのみで集約内部にあるオブジェクトを参照することができないようにします。
外部からの参照を集約ルートだけにして、集約内部への不正な操作を禁止し、ドメイン内の整合性を保つことができます。
宅配便の例
ここでは集約である配達(集約ルート)・配達先・配達状況のオブジェクトがあるとします。
配達先の変更は配達状況が「配達完了」以外なら可能というドメインルールがあります。
もし集約ルート外からのアクセスを許可してしまうと、配達状況を無視して変更ができてしまいます。そのため、配達先の変更時は集約ルートである配達オブジェクトが配達状況を確認した上で実行することで整合性を保つことができます。
②集約単位でデータを更新する
一部のデータだけが更新されてしまい、整合性が無くなることがないように、データの更新は集約単位で行います。
しかし、集約の範囲が大きすぎるとDBのトランザクションによるロック範囲も広くなるため、集約の範囲には注意が必要です。
宅配便の例
ここでも集約である配達(集約ルート)・配達先・配達状況のオブジェクトがあるとします。
配達先の変更時は配達状況を「転送中」にするというドメインルールがあります。
配達先の変更だけをDBに反映しても、配達状況が変わらないままになってしまいます。そのため、DBを更新する際は集約である配達単位で行うことで関連のある値の更新漏れを防ぎ、整合性を保つことができます。
ファクトリ
ファクトリとは、オブジェクトを生成する責務を持ったオブジェクトです。
ファクトリを利用することで、オブジェクトを呼び出すクライアントが集約などの複雑なオブジェクトの内部構造を知る必要がなく、オブジェクトの利用だけに集中することができます。
もし複数のクライアントに生成処理が書かれてしまった場合、同じ処理が複数箇所にあるため、変更時の影響が広がってしまいます。またクライアントがアプリケーション層で、生成処理にドメインのルールが含まれていると、ドメイン層にあるべきコードが別の層に記載されてしまいます。
例として、宅配便における集約である配達の生成処理で以下のルールで配達番号が振られるとします。
- {配達地域番号}-{配達日}-{その地域・その日の配達数}
配達オブジェクトを生成する場合、クライアントが事前に上記情報を取得しておく必要があります。しかし生成処理をファクトリにまとめることで、クライアントは生成に必要なルールを知る必要がなく配達オブジェクトを生成することができます。
リポジトリ
リポジトリとは、オブジェクトの永続化と再構築を行うオブジェクトです。
リポジトリによって、クライアント側はデータを保存しているRDBやNoSQLなどへの具体的な操作処理を書く必要がなくなり、リポジトリを呼び出すだけで保存や検索の処理を簡単に実行することができます。
リポジトリを利用しない場合、上述したデータストアに関する処理をクライアントに書く必要があり、インフラストラクチャ層にあるべきコードが別の層に書かれてしまいます。また集約内部のオブジェクトを直接データストアから取得できてしまうため、ドメインルールの整合性が保てなくなってしまいます。
集約の箇所の例と同じように、配達先の変更時は配達状況を「転送中」にするルールがあった場合でも、直接DBから配達先を変更できてしまうと配達状況の変更がされなくなってしまいます。データストアへの操作はドメインのルールが反映されているリポジトリを利用することで、不正な操作を防止し集約の整合性を保つことができます。