この記事を書く目的
DDDを用いた開発ができるようになりたくて、わかりやすい書籍などから学習を進めていますが、頻繁に内容を忘れるので、備忘録として残します。
ふと気になった概念をさっと思いさせる記事が理想です。
読んだ書籍
ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本
こちらの内容をまとめていきます。
著作権なども考え、基本的には書籍の写しはしない方針であり、あくまでこの書籍を知っている、読んでいる前提での記述となります。
評価も高く非常にわかりやすいので、気になった方は是非読んでみてください。
DDDとは
以下の図はクリーンアーキテクチャと書かれていますが、DDDの概要を掴むために参考になります。
DDDでは、Entities(図の黄色部分)と、Use Cases(図の赤色部分)を実装します。
サービスを実現するために必要となるオブジェクトをEntitiesに記述します。
そのEntitiesを使用して、赤色部分でサービスに必要となるユースケースを実現します。
それぞれの円は、その単位で完結しているので、このユースケースを利用するのはMVCのコントローラでも、WinFormでも、CLIでもよくなります。
また、サービスに必要なロジックはUse Cases内で完結しているので、バリデーションなどが散らばって実装されるリスクを防げます。
上記の理由により、中大規模開発時の整合性の担保や、運用保守フェーズでの負荷軽減効果が期待されます。
ドメイン
ソフトウェアの利用者を取り巻く世界や業界。
開発者から見れば、問題解決を行う対象。
ドメインモデル
ドメインの概念の射影。
ドメインに存在する概念をモデリングしたもの。
コードではない。
ドメインオブジェクト
概念として抽出したドメインモデルを、ソフトウェアで動作するモジュールとして表現したもの。
ドメインモデルの知識をコードとして表現したもの。
ドメインモデルとして抽出しても、すべてドメインオブジェクトとしてコードに落とし込むとは限らない。
ドメイン駆動設計の代表的なパターン
- 知識を表現するパターン
- 値オブジェクト
- エンティティ
- ドメインサービス
- アプリケーションを実現するためのパターン
- リポジトリ
- アプリケーションサービス
- ファクトリ
- 知識を表現する、より発展的なパターン
- 集約
- 仕様
値オブジェクト
知識を表現するパターンの一つ。
ドメインオブジェクトの基本。
プリミティブ型以外の、システム固有の値の定義。
- 代表的な値オブジェクトの性質
- 不変である
- 交換が可能である
- 等価性によって比較される
値の変更をしてはいけないが、値の交換はしてもよい(つまり代入)。
比較のためのロジックは、値オブジェクト自身が保持する。
それにより、ロジックの分散を防ぐ。
値オブジェクトとして実装するかの基準として、以下が役立つ
- そこにルールが存在しているか
- それ単体で取り扱いたいか
特にドメインモデルとして挙げられていなかった場合などに、基準として有効
ドメインモデルとして挙げられていなかった概念を値オブジェクトとして実装した場合は、ドメインモデルへ反映させ、フィードバックする。
値オブジェクトは振る舞いを持つので、オブジェクト生成処理に、ガード節を記述することができる。
これにより、値オブジェクトは自身に関するルールを読み手へ伝えられるようになる。
エンティティ
知識を表現するパターンの一つ。
値オブジェクトと対をなすドメインオブジェクト。
ライフサイクルを持つ。
identityにより識別される。
- エンティティの性質
- 可変である
- 同じ属性であっても区別される
- 同一性により区別される
書籍には記載されていないが、エンティティには個性があるという風に感じた。
エンティティは可変の性質が存在するため、属性を変更したい場合は、その振る舞いを通して属性を変更することになる。
属性を変更する振る舞いには、ガード説が記述される。
全ての属性を必ず可変にする必要はない。
識別子として扱う属性は可変にしない。
エンティティ同士の比較ロジックは、値オブジェクトと同様に、エンティティが保持する。
それにより、ロジックの分散を防ぐ。
ドメインサービス
知識を表現するパターンの一つ。
値オブジェクトやエンティティに記述するには、違和感を感じてしまう振る舞いを記述するためのドメインオブジェクト。
しかし、その気になれば全部ドメインサービスに記述することもできてしまい、値オブジェクトやエンティティのドキュメント的側面を低下させてしまう恐れがある。
これらによってドメインオブジェクトが自身の情報を読み手へ伝えられなくなった状態を、ドメインモデル貧血症と言う。
自身の属性を変更するような振る舞いを持たない(状態を持たない)。
極力ドメインサービスへの記述は避けるのが良い。
値オブジェクトやエンティティと絡めたドメインサービスの実装例は、書籍の4.4を参照してください。
尚、ここの実装例では、今まで説明してきた処理の実装と、データベースへの問い合わせ処理が混在していることにより、可読性が落ちることを指摘しています。これが次のリポジトリの実装メリットへと繋がります。
本来であれば、データストアへの操作に関する記述は、ドメインサービスではなくリポジトリに記述されるのが理想です。
リポジトリ
アプリケーションを実現するためのパターンの一つ。
データの永続化や再構築を担う。
オブジェクトのインスタンスを保存したいときは、直接データベースへ保存はせずに、リポジトリに依頼する。
データベースからデータを取り出し、インスタンスを再構築したいときも、リポジトリに依頼する。
リポジトリはインターフェースとして定義し、必要とされる振る舞い(CRUD等)の実装を強制し、提供する。
書籍では、ここでnullを扱うこととその回避方法について言及しています。
本記事では割愛しますが、null落ちに苦しめられたときに役に立つかもしれません。
書籍では、ここで以下のリポジトリ実装パターンの例を示しています。
- SQLによるリポジトリ実装
- インメモリによるテストが容易なリポジトリ実装
参照の扱いに対する注意点が記載されています。 - ORMによるリポジトリ実装
C#のEntityFrameworkを使った例です。
アプリケーションサービス
アプリケーションを実現するためのパターンの一つ。
ドメインオブジェクトを協調させてユースケースを実現する。
UMLで起こしたユースケース図の機能を、ドメインオブジェクトを使って実現していく。
実際にどこで何のインスタンスを生成しているかなどは書籍を参照してください。
多分ここの処理がイメージできれば自分で実装することができそう。
データベースのデータを取得する時、ドメインオブジェクトとして値を返すことはリスクを伴う。
アプリケーションサービスを利用するクライアントが、ドメインオブジェクトのメソッドを呼び出せる状態を招く。
これを防ぐために、データ転送用オブジェクト(DTO Data Transfer object)にデータを移し替える方法がある。
データベースのデータを更新する時、項目ごとに実装を分けるか、1つの処理として実装するかの選択がある。
コマンドオブジェクトを用意することで、項目ごとに実装を分けないで済む。
ドメインのルールは、アプリケーションサービスに記述してはいけない。
凝集度を測る方法としてLCOMの紹介がされている。アプリケーションサービスの凝集度は高いほうが良いが、絶対ではない。
凝集度を高めるために分割されたクラスは、読み手に対するまとまりというメッセージ性を失う。
まとまりを表現するために、パッケージによってまとめる。
アプリケーションサービスのインターフェースを用意することで、アプリケーションサービスを利用するクライアントへモックを提供することができ、クライアント側の作業を同時並行で行えるようになる。
アプリケーションサービスは、振る舞いを変化させる目的で状態を保持しない。
なのでクライアントは、状態を気にせずにアプリケーションサービスを利用できる。
アプリケーションサービスは、ドメインオブジェクトの操作に徹する。
ファクトリ
アプリケーションを実現するためのパターンの一つ。
オブジェクト生成に関する知識に特化したオブジェクト。
複雑な生成処理であったとしてもファクトリに知識を集約できる。
生成したいオブジェクトのコンストラクタが複雑になってきた時も、ファクトリによって単純化を行う動機になる。
生成処理の記述が分散してしまうことを防ぐ。
テスト実行時と本番運用時で採番ルールを切り替えたいとき、ファクトリのインターフェースを用意して、オブジェクト生成過程を切り替えられるようにする。
それにより、テスト実行時はメモリ上で採番し、本番運用の時はデータベースのシーケンスで採番するなどの切り替えが可能になる。
後続の開発者がファクトリの存在に気付かずに、直接コンストラクタを呼び出してオブジェクト生成するリスクがあるので注意する。
集約
知識を表現する、より発展的なパターン。
データを変更するための単位として扱われるオブジェクトの集まり。
不変条件を維持する単位として切り出され、オブジェクトの操作に秩序をもたらす。
境界とルートが存在する。
- 境界
- 集約に含まれるオブジェクトを定義する
- ルート
- 集約に含まれるオブジェクトで、外部からの集約に対する操作はすべて集約ルートを経由して行われる。
集約の内部に存在するオブジェクトは、外部へ公開したり、操作を許したりしないことで、不変条件を維持する。
集約ルートの事を、AR(Aggregate Root)という。
集約と集約が関連する場合があるが、無理に一つの集約にしたりはしない。
集約と集約が関連する場合は、変更の単位を意識して境界を設定する。
集約を表す図は境界を表すものであって、コードに対する正確性は表現しない。
リポジトリは変更の単位である集約ごとに用意される。
集約は変更の単位なので、大きくなるほどトランザクションのロック範囲も大きくなる。
なので、集約の大きさはなるべく小さい方が良い。
複数の集約を同一トランザクションで操作するのは避ける。
仕様
知識を表現する、より発展的なパターン。
オブジェクトの評価を行うオブジェクト。
オブジェクトに関数として実装した評価処理が複雑になってしまい、本来の趣旨がわかりにくくなってしまった時に、仕様として切り出す。
この関数として実装した評価処理とは、コンストラクタのガード節などよりかは、バリデーション処理のような類を指す。
複数の集約が絡み合って整合性を保持している場合、集約の数だけのリポジトリが用意されていることになる。
しかし、リポジトリ同士はお互いの操作ができる状態にない。
複数のリポジトリから情報を取得して整合性を評価できるのは、複数のリポジトリインスタンスを保持しているアプリケーションサービス上のみになってしまう。
アプリケーションサービスにドメインの知識が漏れている状態になり、ロジックの分散を招く。
アプリケーション上でのリポジトリ同士の操作による解決を避け、値オブジェクトやエンティティが直接リポジトリを操作する方法を避けるために、仕様で実装する。
評価処理に必要なリポジトリやドメインオブジェクトを保持する仕様インスタンスを、アプリケーションサービス上でインスタンス化して使用する。
仕様もドメインオブジェクトであるため、内部で入出力を伴うリポジトリ操作を行うのは避けたほうが好ましい。
その場合は、アプリケーションサービス上で入出力を伴うリポジトリ操作が行われ、それによって得られたデータが仕様に渡される。
リポジトリに検索処理を実装する際、取得対象データが複雑な条件を持つために、複雑な検索クエリが実装されることがある。
その複雑な検索条件はドメイン知識であり、リポジトリに漏れ出している状態になる。
これを解決するために、仕様とリポジトリを組み合わせて使用する方法がある。
仕様に検索条件を記述し、それを使って対象データに絞り込みをかける。
また、リポジトリに仕様を引き渡してメソッドを呼び出させることにより対象となるオブジェクトを抽出する方法については書籍を参照してください。
その他
- 依存関係
- ServiceLocator
- IoC Container
- コンストラクタインジェクション
- メソッドインジェクション
- ServiceCollection
- ServiceProvider
- MVCにDDDを組み込んだ実装例
- Startup時にIoC Containerにて依存関係を設定
- ControllerのActionにてアプリケーションサービスを呼び出す
- コントローラの責務
- 自動採番処理を採用した際のリスク
- データベース保存を行うまで識別子が確定しない
- データベース保存を行った後に識別子を取得してオブジェクトにsetする必要がある => セッターが必要になり、意図しない識別子変更が行われる可能性が出てくる
- トランザクション
- TransactionScope
- Entity Framework
- AOP(Aspect Oriented Programming)
- ユニットオブワーク
- デメテルの法則
- メソッドを呼び出す4つのオブジェクト
- オブジェクト自身
- 引数として渡されたオブジェクト
- インスタンス変数
- 直接インスタンス化したオブジェクト
- メソッドを呼び出す4つのオブジェクト
- 通知オブジェクト
オブジェクトの内部データを非公開にした場合、リポジトリで永続化する際に値の取得ができない。
それを解決するために、エンティティとリポジトリの間に通知オブジェクトを挟む。 - 遅延実行
IEnumerable型が戻り値の実装をする - アクセス限定子
- IDによるコンポジション
- 結果整合性
一時的にデータ不整合を許容する。
整合性を取る処理とタイミングを別で設け、最終的な整合性を担保する。 - ファーストクラスコレクション
あるドメインオブジェクトの集合に特化したコレクション
独自の計算処理をメソッドとして定義される - リードモデル
- CQS(Command Query Separation)
- CQRS(Command Query Responsibility Segregation)
- その他のアーキテクチャ
- 軽量DDD
- ドメイン駆動設計のモデリング手法
- ユビキタス言語
- コンテキストマップ
- パッケージ構成 例
- プレゼンテーション
- アプリケーション
- アプリケーションサービス A
- アプリケーションサービス B
- ドメイン
- Models
- モデル
- エンティティ
- 値オブジェクト
- リポジトリインターフェース
- 仕様
- ファクトリインターフェース
- モデル
- Services
- ドメインサービス
- Shared
- 仕様インターフェース
- Models
- インフラストラクチャ
- 技術基盤 A
- 集約 A
- リポジトリ実装
- ファクトリ実装
- 集約 B
- 集約 A
- 技術基盤 B
- 技術基盤 A
- ソリューション構成 例
- すべてを別のプロジェクトにする
- アプリケーションとドメインだけ同じプロジェクトにする
終わりに
重要な項目に絞ってわかりやすくまとめようとしましたが、あれもこれも大事な内容ばかりで、思ってたよりも記述量が膨れてしまいました。
書籍のはじめにの段落でも記載されていますが、ドメイン駆動設計を理解するための専門用語の多さや、必要になる予備知識、連鎖的に必要になる知識が、この簡単にまとめきれない現象の背景にあるように感じます。
これがドメイン駆動設計を習得しにくくさせている訳ですが、それでもやはりこの書籍は分かりやすく、まとめる過程でかなり腹落ちさせることができました。
次は実践に移る段階かと思うので、自分でこの記事に立ち返りつつ、引き続きドメイン駆動設計の習得を目指します。