ドメイン駆動設計~モデリング/実装ガイド~
DDDてなに?
-
DDD:モデリングによってソフトウェアの価値を高めることを目指す開発手法
-
モデル:問題解決のために、物事の特定の側面を抽象化したもの
- ドメインモデル:ドメインの問題を解決するためのモデル(本書でのモデルはこっち)
- データモデル:データの永続化方法を決めるためのモデル
-
抽象化:取り込む要素を取捨選択するプロセス->その成果物がモデル
-
いいモデルとは? -> 問題を解決できるモデル
⇔ どんなに可読性拡張性が高くバグがなくても、問題が解決できないモデルはよくないモデル
例:採用管理アプリケーション
・書類選考の前に面談があるのに設定できない
・面接は3次、4次もあるのに追加できない
・求人関係ない応募も許可したいのに設定できない
=> 現場の運用に適用できず問題解決できない
いいモデルをつくるには?
- ドメインエキスパートと早めに会話することでずれをなくす
- 運用して得た発見をモデルに還元
- モデルの改善
- ユビキタス言語:発見したモデルの言葉をすべての場所で使うという指針. In Everywhere
- 境界付けられたコンテキスト:あるモデルを同じ意味で使い続ける範囲を定義したもの
モデル→ソフトウェアの継続的な反映
- モデルとコードをなるべく近づけることで正しく反映させる
- モデル表現を「ドメイン層」として隔離し、モデル以外の処理と分ける
- 戦術的設計パターン:エンティティやリポジトリのパターンのみ取り入れる⇒軽量DDD
解決したい課題を明確にする
- 手段を目的にしない
- 課題ドリブン
小さく始めて小さく失敗
- 実装して初めて気づくことは多い
- 試行錯誤のサイクルを小さくし、少しずつ成功体験を積む
DDDが向いているプロジェクト
⇒ドメインが複雑な場合
向いていないプロジェクト
⇒非常にシンプルな場合(単純なCRUDしかないなど)
DDD固有のこと
集約
集約:必ず守りたい強い整合性を持ったオブジェクトのまとまり
- ドメインモデリングにおいてすべてのオブジェクトは集約に所属
- 集約単位でリポジトリに渡し、1トランザクションですべてのオブジェクトを更新するようにする
集約の範囲 - 集約の範囲が大きすぎると、DBに対して不必要に大きなロックを取る
境界付けられたコンテキスト
- 言葉は場面や人によって違う意味を持つ(例:同じ「商品」でも販売と配送側で異なる認識や属性を持つ)
- コンテキストを分け、それぞれの中でモデルや言語を統一する
- 販売コンテキストにおける「商品」:商品名、売値、在庫数
- 配送コンテキストにおける「商品」:商品名、配送先、配送状況
設計の基本原則
凝集度・結合度とは
ソフトウェアの品質を表す指標
高凝集・低結合にすることで以下が改善する
- コードが理解しやすくなる
- 拡張しやすくなる
- バグが入りにくくなる
- 同じコードを再利用しやすくなる
- テストしやすくなる
また、基本的にモジュール単位(クラス、レイヤーなど)で考える
凝集度
- モジュールにおいて責務、データ、ふるまいが明確に定まっており、かつ明確に関連しているか
- 高い方がいい
結合度
- 複数のクラス同士がどれだけ依存しているか
- 低い方がいい
- 低結合⇒それぞれの実装が変わっても大きく影響を受けない
1モジュール内は高凝集、複数モジュール間は低結合にする
⇒可読性保守性が高くなる
アーキテクチャ
従来の3層アーキテクチャには問題点あり
問題を解消するアーキテクチャをいくつか考える
3層アーキテクチャ
- プレゼンテーション層:クライアントとの入出力
- ビジネスロジック層:ユースケース、ドメイン知識
- データアクセス層:DBとの入出力、ドメイン知識
問題点
- ビジネスロジック層に「ユースケース」と「ドメイン知識」があることで、責務過剰、低凝集になる
- モデルにもドメイン知識が書かれることで、2層間の結合度も高まる
(3層アーキテクチャの成り立ちなども知りたい)
レイヤードアーキテクチャ
ユースケースの層とドメイン層に分けるという目的
ビジネスロジック層→アプリケーション層、ドメイン層
問題点
- ドメイン層にリポジトリを置く場合、ドメイン層がインフラ層に依存してしまう。(インフラ関連で変更があった場合、ドメイン層でも変更が必要になる)
オニオンアーキテクチャ
インフラ層→ドメイン層の流れで依存させるようにしたもの(依存関係逆転の原則)
具体的には、ドメイン層にインターフェース、インフラ層に実装クラスを定義
- ドメイン層が特定の技術に依存しなくなる⇒高凝集・低結合
ドメイン層
- ドメイン知識を表現し、ほかの層へ依存させないようにする
- ドメインオブジェクト(エンティティ、値オブジェクト、ドメインイベント)
- 上記を利用するクラス(リポジトリのインターフェース、ドメインサービス、ファクトリー)
ユースケース層
- ドメイン層の操作を使ってユースケースを実装
- 特定のクライアントに依存しないようにする
プレゼンテーション層
- 外部との入出力(JSONで返したりHTMLをレンダリングして返す)
- コントローラー、外部との入出力を定義するクラス
インフラストラクチャ層(インフラ層)
- リポジトリの実装クラス
コントローラー=外部とのアダプターであり、アプリケーション(ユースケース層)を呼び出す
ヘキサゴナルアーキテクチャ(ポートアンドアダプターアーキテクチャ)
専用のポートとアダプターを作成して外部と通信する
クリーンアーキテクチャ
ヘキサゴナルアーキテクチャとの違い
アプリケーション層→ユースケース層、エントリー層
アダプター層→インターフェースアダプター層
オニオン、クリーンの比較
以下の理由から、DDDにはオニオンアーキテクチャが向いてそう
- クリーンは要素が多くwebアプリケーションにおいては不要なものもある
- クリーンの「エンティティ層」の責務は「企業一般で適用できるビジネスルール」であり、これはコンテキストが広く、DDDの思想とずれる
境界付けられたコンテキストの実装
- 1コンテキスト1アプリケーションがシンプル(マイクロサービス)⇒コスト大
1コンテキスト1アプリケーション
マイクロサービス同士の通信は、同期と非同期通信に分かれる
- 同期通信:ネットワーク経由のダイレクトコール(REST APIなど)
- 非同期通信:メッセージキューを利用したイベント通信(AWS SQSなど)
⇒「リクエスト結果を同期的に取得したい」「非同期にして先方のサービスがダウンしていても大丈夫なようにしたい」というような要件を考慮して決める
複数コンテキスト1アプリケーション
コンテキストごとにパッケージとして実装
⇒あとからアプリケーション分割しやすくなる
ドメイン層の実装
設計するのは以下の2種類
ドメインモデルを表現するもの(ドメインオブジェクト)
- エンティティ(モノ)
- 値オブジェクト(モノ)
- ドメインイベント(コト)
ドメインオブジェクトを使用するもの
- リポジトリ
- ファクトリー
- ドメインサービス
エンティティ
- 識別子(ID)で同一判定するオブジェクト
- ドメイン内のさまざまなビジネスの実体の概念をモデル化する
- 可変
値オブジェクト
値を表現するのみの目的
- 不変に保てる(変更できない)
- 値同士が等しいか比較できる
- 副作用がない(ほかの状態に予期せぬ影響を与えない)
(参考)
- https://zenn.dev/yamachan0625/books/ddd-hands-on/viewer/chapter8_value_object
- https://qiita.com/2san/items/b52c0828e1fec03ebed1
ドメインサービス
オブジェクトとして表現すると無理があるもの
例:ほかのオブジェクトの情報も必要とするものなど
なんでもドメインサービスに書けるが、ファットクラスになるのでできるだけドメインオブジェクトに記述する
リポジトリ
集約単位で永続化層へのアクセスを提供する
⇒整合性を保証するため
- 必ず集約ルートのエンティティとして返す
- ほかの子オブジェクトは集約ルートからインスタンス参照
✖子オブジェクトを直接返したりリポジトリを別途定義しない - インターフェースのみ定義する
単体テストがしやすくなる
ファクトリー
- オブジェクト生成ロジックが複雑な場合や他の集約を参照する必要がある場合に用いる
- リポジトリを参照する
その他
- ドメイン層として独立させることで、ドメインの変更を修正しやすくする(影響はテストで対応)
- ※Q&Aは実装の練習をしてから再度見直す
ユースケース層(アプリケーション層)の実装
ドメイン層のメソッドを組み合わせてユースケースを実現する
例:タスク延期ユースケース
大まかな流れ(ユースケース)の流れのみで、処理の仕方はドメイン層に隠蔽している
public class TaskPostponeUseCase {
@Autowired
private TaskRepository taskRepository;
@Transactional
public void postpone(Long taskId) {
Task task = taskRepository.findById(taskId);
task.postpone();
taskRepository.save(task);
}
}
ユースケースからの戻り値
ユースケース層で専用の戻り値クラスに詰め替えて返すのがよさそう
メリット
- ドメイン層に変換などの処理を書かなくて済む
- 上記に関連して、ドメイン層の影響をプレゼンテーション層が直接受けなくなる
戻り値クラスの名称例
- ドメイン→ユースケース:ドメインオブジェクト
- ユースケース→プレゼンテーション:DTO(Data Transfer Object)
- プレゼンテーション→クライアント:Response
その他
- ユースケースクラスは「1クラスに1パブリックメソッド」で分割
- 複数書くとそれだけ依存先が増えたり対応関係が分かりにくくなり、凝集度と保守性が下がる
- 共通処理が欲しくなったらutilクラスとして切り出す