読書メモ:ドメイン駆動設計 モデリング/実装ガイド(前半)


会社で「ドメイン駆動設計 モデリング/実装ガイド」の読書会に参加しました。
連休中に内容を見返すと、結構忘れていたので、内容をメモをすることで思い出そうと思います。

後半はこちら


第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コンテキストとするのが簡単
    • コンテキストごとのマイクロサービスとなる
    • ただし実装コストは高くなる
  • パッケージで区切ってしまう
    • 逆に複雑になる時もある
    • パッケージで区切る方法で始めて、後にサービスを分割する方法もアリ

後半はこちら