会社で「ドメイン駆動設計 モデリング/実装ガイド」の読書会に参加しました。
連休中に内容を見返すと、結構忘れていたので、内容をメモをすることで思い出そうと思います。
後半はこちら。
第1章 DDD概要
DDDに取り組むにあたり、言葉の定義によって混乱することが多いので、ここで以下のように定義する。
DDDとは
DDDとは、ソフトウェア開発手法の一つ。
ソフトウェアで問題解決をしようとしている対象(:ドメイン)をモデリングすることでソフトウェアの価値を高めようとする。
モデルとは
問題解決のために、物事の特定の側面を抽象化したもの。
- ドメインモデル(モデル):ドメインの問題を解決するためのモデル
- データモデル:データの永続化方法を決めるためのモデル
モデルの例
モデルは、特定の事象を抽象化したもの。
例えば、履歴書の場合、以下の情報が読み取れる
- 名前
- 経歴
- 志望理由
- 顔写真
- 筆跡、筆圧
- 履歴書のメーカー
- 履歴書を書いた時の気持ち
これらの情報から、どのような情報をソフトウェアに取り込むかを決める過程が「抽象化」。
抽象化によってできた成果物が「モデル」。
良いモデル、良くないモデル
良いモデル
良いモデルとは、問題を解決できるモデル。
理解がしやすい、きれいに見えるなどは二の次。あくまでモデルは、問題解決のために、事象を抽象化したものである。
良くないモデル
モデルのステータスが追加できない、発生するイレギュラーケースに対応できないなど、実務での問題を解決できないモデルは、いかにテストがそろっていようと、バグが少なくても、評価されない。
良いモデルを作るには
ドメインエキスパートと会話をする
そのドメインに詳しい人(:ドメインエキスパート)と会話をし、モデルに知識を反映することが大事。
ただし、ドメインエキスパートは開発のエキスパートではないため、次の視点も必要になる。
運用して得られた発見をモデルに還元する
運用して発見した改善点は、細かく追加していく。
モデルは最初から完成せず、徐々に改善していくもの。
(そのため、コードは頻繁な変更に耐えられる作りにする必要がある。)
DDDの問題解決のアプローチ
以下のアプローチを行うことが重要。
- モデルを継続的に改善
- モデルを継続的にソフトウェアに反映
モデルの継続的な改善
ドメインについての理解を深め、モデルを継続的に改善する。
- ユビキタス言語
- 発見したモデルに関する物事を、プロダクトに関わる全ての場所において同じ言葉で表現すること
- ビジネスサイドの人とも共有する
- 会話でも、ドキュメントでも、コードでも、同じ言葉を使う
- 境界づけられたコンテキスト
- あるモデルを同じ意味で使い続ける範囲のこと
- 第3章参照のこと
モデルを継続的にソフトウェアに反映
- モデルを直接表現するコードへの反映
- 極力モデルとコードを近づける
- モデル表現を隔離するアーキテクチャ
- モデルを表現するコード意外にもUIやDBとのやりとりを表現するコードが存在する
- モデル表現と、それ以外を表現するコードが混ざるとややこしい
- モデルの表現を行うコードを「ドメイン層」として隔離する
取り組む上で重要な考え方
- 課題ドリブン
- 意思決定の際には、課題を明確にして解決できるかを考える
- ルールより課題解決ができるかを重視する
- 小さく初めて、小さく失敗する
- モデリングに1週間かけるより小さく実装する方が良い
- 小さい部分から始めて、少しずつ成功体験を積み上げる
第2章 モデリングから実装まで
ドメインモデリング
DDDでは、モデリングの方法や、アウトプット形式が迷うポイント。
今回は比較的シンプルで、小さく始めて効果を出しやすいユースケース図とドメインモデル図を利用する。
ユースケース図
ユースケース図は、「ユーザの要求に対するシステムの振る舞いを定義する図」。
ユーザを定義し、棒人間で表して、ユーザが行う動作を吹き出しで「〇〇を××する」といった形式で書き出す。
さらに、今回のモデリングにおけるスコープも、図中に明記する。
ユースケースを書き出すことで、どのようなモデルを作れば良いか判断できるようにする。
ユースケースが具体化されていないと、モデルで解決すべき問題点が明確にならず、良いモデルとそうでないモデルの区別が付かなくなってしまう。
また、モデリングにおけるスコープを明記することで、議論の発散を防ぐ。
ドメインモデル図
ドメインモデル図は、簡略化したクラス図。
- 書くこと
- オブジェクトの代表的な属性
- 「ルール」、「制約(ドメイン知識)」を示した吹き出し
- 箇条書きなど
- 状態遷移図などでもOK
- オブジェクト同士の関係
- 集約内の参照は「-◆」で表す(インスタンス参照)
- 集約外の参照は「→」で表す(ID参照)
- 多重度の定義
- 集約範囲の定義
- 具体例などもあれば
- 書かないこと
- メソッド
(ここは、書籍のP24にある図を見た方がわかりやすい。)
ドメインモデルの実装
作成したドメインモデルを、コードに落とす。
ドメインモデル貧血症
ドメインモデルを実装するためのオブジェクトでありながら、ドメイン知識をほぼ持っていないオブジェクトのこと。
書籍ではドメイン貧血症のコードをリファクタリングする。
アーキテクチャ
まず、全体のアーキテクチャを決定する。
レイヤーを定義して、各レイヤーに実装するクラスの方針を定義する。
(ここでは、オニオンアーキテクチャを採用)
詳しくは第5章参照。
ドメイン貧血症のコード
(このメモを書いている私の都合でPHPで書きます...)
public class Task
{
private id;
private taskStatus
private name;
private duDate;
private postponeCount;
// 全ての属性には、publicなsetterとgetterが存在する
}
- 問題点1:不整合なデータをいくらでも作り出せる
- 全てのsetterがpublicであるため、自由にデータを作り出せてしまう
- 仕様の把握のためには、多くのクラスやコードを辿る必要がある
- ステータスや期日変更にはどんなパターンがあるかこのクラスだけではわからない
ドメインモデル知識を表現した実装
ドメイン貧血症を防ぐためには、以下に注意する
- ドメイン知識はドメイン層で実装する
- ユースケース層で実装しない
- ユースケースには「なにをしたいか(What)」のみが記載されるようにする
- 想定外の操作が行われないようにする(例えばpublicなsetterは作成しない)
ドメイン層オブジェクト設計の基本方針
以下の2点に従う。
- ドメインモデルの知識を対応するオブジェクトに書く
- 常に正しいインスタンスしか存在させない
- 生成条件を強制する(なにもないコンストラクタは実装しない)
- 内部状態の変更を強制する(ミューテーションを起こすメソッドを正しく実装し公開する)
第3章 固有のモデリング手法
集約
集約とは、「必ず守りたい強い整合性を持ったオブジェクトのまとまり」のこと。
以下の2ルールにしたがって定義する。
- 強い整合性確保が必要なものを集約にする
- オブジェクトの属性が他のオブジェクトに強く影響する場合など
- トランザクションを必ず1つの集約にする
- 整合性を守るため、集約単位でリポジトリからデータを取り出したり、更新したりする
集約ルートとは
各集約には、ルートとなるオブジェクトを1つ決め、集約ルートと呼ぶ。
集約の決め方
- 集約の境界は、機械的な決め方は存在しない
- 整合性確保の重要性を加味して決める
- 複数集合間で整合性を確保しないわけではないので、整合性確保が必要だから必ず集合にする必要はない
- トランザクション範囲の適切さも加味する
- 大きすぎる集合だと、DBへ反映するときに負荷がかかる
境界づけられたコンテキスト
同じシステム内でも、1つの事象をすべて1モデルで表すのには無理がある。
そのため、適切な範囲でモデルがカバーする範囲を分割する。
このモデルがカバーする範囲のことを「コンテキスト」と呼ぶ。
(モデルを別にして、コンテキストが変わった時は情報を詰め替えるイメージ??)
第4章 設計の基本原則
凝縮度
凝縮度とは、1クラスでの「責務、データ、振る舞いの関連の強さ」の尺度。
なんでもできるクラスを作るのではなく、責務に沿った内容が集約されているかどうか。
一般的に高凝縮がいいとされる。
責務を明確にするため、「このクラスは何をするクラスか」を明確にすべき。
- ❌ 低凝縮に陥る例
- クラス名自体が責務を曖昧にしている
- クラス名と関連が明確でないデータ名
- 目的語が欠如したメソッド名(「なにを」インクリメントするのか、など)
- 保持しているデータと関係ないロジック
結合度
結合度とは、複数のクラス同士が依存している度合い。
一般的に、低結合が良いとされている。
結合度が高いと、依存先クラスの修正による影響を受けやすくなる。
また、実現したい処理を、多くのクラスを組み合わせないと実現できない場合は、すでに結合度が高くなっていると考えられる。
高凝縮・低結合で得られるメリット
-
コードを理解しやすくなる
-
コードの修正/拡張がやりやすくなる
-
バグが含まれにくくなる
-
コードを再利用しやすくなる
-
テストがしやすくなる
第5章 アーキテクチャ
DDDで提唱されているアーキテクチャの解説。
3層アーキテクチャ
概要
デメリット
以下の理由で可読性や保守性が下がる。
- ビジネスロジックが膨らむ。(低凝縮になる)
- ビジネスロジックがユースケースとドメイン知識両方を担っているため
- ビジネスロジック層とデータアクセス層が高結合になってしまう
- ドメイン知識がビジネスロジック層とデータアクセス層にあるため
レイヤードアーキテクチャ
概要
3層アーキテクチャのビジネスロジック層を2つに分けたもの。
メリット
- レイヤーごとに責務がはっきりし、可読性、保守性が上がる(高凝縮になる)。
デメリット
- ドメイン層がインフラ層に依存してしまう
- ドメイン層にリポジトリをおく場合、DBやORマッパーに依存した実装になる
モデルを継続的に改善するためには、ドメイン層はインフラ技術に依存すべきではない。
オニオンアーキテクチャ
概要
レイヤードアーキテクチャから、ドメイン層とインフラ層の依存関係を逆転させたもの。
図中の白矢印は、インターフェイスの実装を示す。
名前の通り、丸型で表すこともできる。
この場合、依存関係は「外側から内側」のみ許される。
プレゼンテーション層、インフラ層、テストは、それぞれアダプターを通して外部と通信する。
(スマホアプリ、ブラウザ、RDB、ファイル、外部サービス、結合テスト など)
- ドメイン層
- ドメイン知識の表現
- ドメイン層の独立で他の層への依存を持たせない
- 整合性が保てるメソッドのみ外部に公開する
- 実装するクラスは以下の通り
- エンティティ
- 値オブジェクト
- ドメインイベント
- リポジトリインターフェイス
- ドメインサービスファクトリー
- ユースケース層
- ドメイン層のメソッドを使ってユースケースを実現する
- 特定のクライアントに依存させない (クライアントに合わせるのはプレゼンテーション層)
- 実装するクラスは以下の通り
- ユースケースクラス
- プレゼンテーション層との入出力定義クラス
- プレゼンテーション層
- アプリケーション外部との入出力を実現
- 実装するクラスは以下の通り
- コントローラ
- アプリケーション外部との入出力を実現するクラス
- インフラ層
- 下位レイヤーのインターフェイスを実装する
- 実装するクラスは以下の通り
- リポジトリ実装クラス
メリット
- ドメイン層を特定の技術に依存させずに済む
ヘキサゴナルアーキテクチャ
概要
アプリケーションが外の世界と通信する際には、専用のポートとアダプターを作成して通信させるアーキテクチャ。
レイヤーの分け方が異なるが、基本的にはオニオンアーキテクチャの思想を踏襲したもの。
(別に六角形である意味はない。)
クリーンアーキテクチャ
概要
オニオンやヘキサゴナル、その他のアーキテクチャを受けて概念を結合しようとしたアーキテクチャ。
なお、筆者のおすすめはシンプルさと元々のDDDが目指す思想との差異から「オニオンアーキテクチャ」だそう。
境界づけられたコンテキストの実装
- 1アプリケーション1コンテキストとするのが簡単
- コンテキストごとのマイクロサービスとなる
- ただし実装コストは高くなる
- パッケージで区切ってしまう
- 逆に複雑になる時もある
- パッケージで区切る方法で始めて、後にサービスを分割する方法もアリ
後半はこちら。