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

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

September 1, 2022

以下の本を読んだ。あとで見返したいところについてメモを残しておく。

第 2 章:モデリングから実装まで #

ドメイン層オブジェクト設計の基本方針 - P.32 #

ドメイン層のオブジェクトを設計する際に、重要な基本方針があります。

  • ドメインモデルの知識を対応するオブジェクトに書く
  • 常に正しいインスタンスしか存在させない

この2つを守ると、非常に保守性の高いコードにすることができます。

常に正しいインスタンスしか存在させない #

もし、ドメインオブジェクトが、整合性の保証されたインスタンスしか存在できなくなったらどうなるでしょうか。インスタンスをどのタイミングで永続化しても、確実にデータの生合成が保証されることになります。これは実装上非常に強い安心感に繋がります。

これを実現するためには、次の2つを行います。

  • 生成条件の強制
  • ミューテーション条件の強制

生成条件の強制 #

すべてのインスタンスはコンストラクタ、もしくはファクトリーメソッドを経由して生成されます。そのため、これらのメソッドがすべて正しい条件を矯正できれば、新規作成されたインスタンスはすべて生合成が保証されていることになります。

これに反しているのが、何のロジックもないデフォルトコンストラクタです。すべての項目に値が入っていないインスタンスは、整合性を保証されたものではありません。

ミューテーション条件の強制 #

生成メソッドの制御により、インスタンスが正しい状態で生成されることが保証できました。あとは、すべての内部状態の変更、つまりミューテーションが正しければ「常に正しいインスタンスしか存在させない」ことが可能になります。そのために、正しいミューテーションを起こすメソッドのみ外部に公開するようにします。

これに反しているのが、すべての項目に対するセッターです。

集約とは - P.38 #

集約とは、「必ず守りたい強い整合性を持ったオブジェクトのまとまり」を表します。

集約の設計/実装時のルールは2つあります。

  • 強い整合性確保が必要なものを1つの集約にする。
  • トランザクションを必ず1つにする。

第 6 章:ドメイン層の実装 #

ドメインサービス - P.68 #

ドメインサービスは、「モデルをオブジェクトとして表現すると無理があるもの」の表現に使います。例えば、集合に対する操作などです。

よく使われるのはユーザーのメールアドレスを更新する際の重複チェックです。「指定されたメールアドレスはすでに使われているか?」と尋ねたとき、その知識を1つのユーザーオブジェクト自身が答えられる、とするのは無理があります。自分自身のメールアドレスを知っていても、他のオブジェクトの状況については情報を持っていないからです。こういう場合に、ドメインサービスを使用します。

ただし、極力エンティティと値オブジェクトで実装するようにして、どうしても避けられない時のみドメインサービスを使うようにしてください。ドメインサービスは手続き的になるので、従来の「ビジネスロジック層」の感覚で書いてしまいがちです。そうすると、結局従来のようなファットなクラスが異なるレイヤーに現れただけ、という結果になってしまいます。

リポジトリ - P.68 #

リポジトリとは「集約単位で永続化へのアクセスを提供するもの」です。集約単位でという制約の理由は、ひとまとまりで整合性を確実に保証するためです。

集約のオブジェクトの親となるオブジェクトを集約ごとに1つ決め、そのオブジェクトを「集約ルート」と呼びます。リポジトリは集約ごとに1つだけあり、リポジトリに渡すもの、リポジトリから返されるものは必ず集約ルートのエンティティになり、その他の子オブジェクトは集約ルートがインスタンス参照した状態で常に扱います。リポジトリから子オブジェクトを直接返したり、子オブジェクト用のリポジトリを別途定義したりはしてはいけません。

リポジトリはインターフェイスがドメイン層、その実装クラスがインフラ層になります。

これにより、ドメイン層は永続化手段(DB の種類やテーブル構造、OR マッパーなど)に関して一切知識を持たなくなり、純粋にドメイン知識だけに集中できるようになります。

設計上のポイントは「リポジトリは List のように扱う」ということです。ユーザークラスの List があった時、その List は「ユーザを登録する」「ユーザを退会状態にする」というメソッドは持たないはずです。新規登録状態のユーザー、退会状態のユーザーを add するという使い方をするでしょう。リポジトリも同じです。List に持たせないようなメソッド、つまりドメイン知識(ルール/制約)を持っていたとしたら、それは責務過剰なのでリポジトリから別のクラスに移譲しましょう。

DB の値からインスタンスを再構成する方法 - P.71 #

質問 #

コンストラクタ内に生成条件(初期状態)を実装した場合、DB から撮ってきた値はどのように反映するのでしょうか。

回答 #

専用のコンストラクタを設けます。このコンストラクタはすべての属性の値をそのまま受け取り、バリデーションをしません。

このコンストラクタは「fromRepository」や「reconstruct」といった命名規則でその意図を明示します。

ソートの実装方法 - P.73 #

質問 #

ソートはどこで行うのが良いのでしょうか?

回答 #

リポジトリのインターフェイスでは、永続化層に依存しない抽象的な実装(enum など)でソート順を引数で指定できるようにします。そして、実際のソート処理のためのクエリ組み立てなどはインフラ層のリポジトリ実装クラスで行います。永続化層が RDB の場合、引数で指定された内容を元に order by 句を作成します。

外部 API とリポジトリの関係 - P.73 #

質問 #

Twitter のような外部 API を利用する時も、リポジトリパターンを使うのが良いでしょうか?

回答 #

外部 API に渡す値、外部 API から取得する値が、ドメインモデルとして意味を持つのであれば、ドメイン層のものとして定義してリポジトリで設計します。

そうではない例として、通知を扱う例を考えます。ユースケースとして通知を行いますが、ドメインモデルとしては定義しないと判断した場合は、ユースケース層に NotificationAdapter といった名前のインターフェイスを定義し、実装クラスはインフラ層に配置します。

外部 API から取得した値の詰め替え方法 - P.74 #

質問 #

API からデータを持ってきて、それをドメインオブジェクトに変換する際は「リポジトリの実装クラス内で API を呼び出し、取得したデータをドメインオブジェクトに変換する」というのが良いでしょうか?それとも「ユースケースクラスで Mapper 的なクラスを呼び出す」のが良いのでしょうか?

回答 #

前者の選択肢が良いです。リポジトリのインターフェイスはドメイン層であり、ドメインの知識としては「どういう条件を指定したらどういうオブジェクトが取得できるか」という定義(What)にだけ関心があり、その How は隠蔽したいのです。API の呼び出し方や、取得結果の戻り値のオブジェクトに変換する方法はあくまでインフラ層の関心ごとになるので、インフラ層のクラスの中で完全に隠蔽するのが望ましいです。

エンティティ同士の紐付け - P.74 #

質問 #

エンティティ間の関係を表すには ID を持たせるのとインスタンスを持たせるのはどちらがよいでしょうか?

回答 #

これは集約の設計に依存します。オブジェクト間の参照関係は、集約内はインスタンス参照、集約外は ID 参照です。

一部カラムを更新するときの扱い - P.75 #

質問 #

エンティティの一部分だけの更新をしたい場合、一部分だけリポジトリに渡すのでは無く、エンティティを丸ごと渡して丸ごと更新する必要がありますか?

回答 #

その通りです。リポジトリは必ず集約単位で更新処理を行います。集約は「必ず守りたい強い整合性を持ったオブジェクトのまとまり」なので、整合性を集約ルートのオブジェクトに管理させます。一部のデータを直接永続化をしてしまうことは、整合性の管理ができなくなってしまうために避けます。

ドメインオブジェクトからリポジトリ操作の可否 - P.75 #

質問 #

ユースケースからではなく、エンティティからリポジトリを利用して、DB からの取得や更新などの操作をしても良いものでしょうか?

回答 #

エンティティが複数の責務を持つことになるので、非推奨です。

リポジトリを通じた削除方法 - P.76 #

質問 #

リポジトリでエンティティを削除する場合、削除対象のエンティティを渡すのか、ID だけ渡すのか、どちらが良いでしょうか?

回答 #

ID だけで良いでしょう。集合に対する操作として、削除したいオブジェクトを識別子で指定して削除する、と言うことは概念としても矛盾がありません。

実装面でも、取得結果を別の用途で使う場合を除いて、削除対象のエンティティキーを一度取得するコストは無駄になるので不要です。

第 7 章:ユースケース(アプリケーション)層の実装 #

ユースケースからの戻り値クラス - P.78 #

ユースケース層からプレゼンテーション層に返す値の型については、以下の2つの方針が考えられます。

  1. 専用の戻り値クラスに詰め替えて返す
  2. ドメイン層のクラス(ドメインオブジェクト)をそのまま返す

長期的に保守性を高めるには、1の方が有利です。1のメリットデメリットを比較すると、以下の通りです。

  • メリット
    • ドメインオブジェクトに、プレゼンテーションに関連する処理が混入するのを妨げる。
    • ドメイン層の修正の影響を、プレゼンテーション層が直接受けなくなる。
  • デメリット
    • ドメインオブジェクトからの詰め替えコストが発生する。

2の選択肢は、1のメリットデメリットの裏返しとなります。

ドメインオブジェクトをプレゼンテーション層に渡してしまうと、表示に関わるメソッド(数値の書式を変換するなど)をついドメインオブジェクトに生やしてしまいがちです。ドメインオブジェクトの責務はドメイン知識(ルール/制約)を表現することなので、表示に関する処理は責務外の処理になります。このような事態を最初から防ぐために、ドメインオブジェクトを返さない、というのは有効な方針です。

最初は、詰め替えコストを嫌がって2の選択肢を取りがちですが、気がついたらドメインオブジェクトが責務外のメソッドで膨れ上がっている(低凝集になっている)という事態を誘発します。それを回避するためには、1の選択肢を取るのが良いでしょう。

ユースケース層の戻り値専用クラスの名称は DDD で特に定義はされていないので、プロジェクトごとに決める必要があります。

本書では、このクラスを DTO(Data Transfer Object)と定義しています。DTO というのはレイヤ間の受け渡しに広く使われがちですが、ユースケースからの戻り値のみに使用し、他のクラスは別の名称に使用します。

ユースケースクラスの分割単位 - P.79 #

質問 #

ユースケースクラスは、どのような粒度で分割するのが良いでしょうか。

回答 #

「1クラスに1パブリックメソッド」とするのが良いでしょう。1クラスに複数のパブリックメソッドを書くと、凝集度が下がり、保守性が下がります。

具体的なデメリットとして、次のようなものがあります。

  • 複数メソッド分の依存クラス(リポジトリなど)やプライベートメソッドを持つことになり、参照関係が追いにくくなる
  • テストクラス側のメソッド数がその数倍に増えて対応がわかりにくくなる

この対応として、パブリックメソッド単位でクラスを分割すると、ユースケースの業種度が高まり、保守性が高まります。共通の処理が欲しくなった場合は、プライベートメソッドではなく、その責務を負ったクラスとして切り出すのが良いでしょう。例えば、共通の変換処理があればコンバーターと名のつくクラスとして切り出し、複数ユースケースクラスで共有する、といったものです。

第 8 章:CQRS #

集約内の一部の値だけ取得したい場合の対応方法 - P.89 #

質問 #

集約内の要素である大きめの値オブジェクトだけデータを取得したい場合はどのようにすれば良いでしょうか?

回答 #

基本的にはリポジトリでドメインオブジェクトを取得し、ユースケースの処理で必要な項目だけ詰め替えれば良いですが、パフォーマンスが問題になる場合は CQRS の導入を検討します。

第 9 章:プレゼンテーション層の実装 #

プレゼンテーション層の処理概要 - P.91 #

プレゼンテーション層のコントローラーと、ユースケースクラスのリクエストとレスポンスは、特定のクラスに依存しない、ピュアなオブジェクト(Java でいう POJO)にします。

レスポンス仕様の定義 - P.92 #

レスポンスの仕様を定義します。HTML レンダリングを行うコントローラーの場合、UI にまつわるもの(色、文字の書式など)を決めるのもプレゼンテーション層の責務です。

特に書式に関するものがこの層の責務であるということは重要です。例えば、ユースケースが数値を表す型で「1000」という数値を返したものを、画面ではカンマ区切りで単位をつけて表示するとします。

この変換に関する責務を持つのは、プレゼンテーション層です。ユースケース層やドメイン層のオブジェクトが、保持する値を表示用にフォーマットするメソッドを持っていたら、それは責務違反となります。凝集度が下がり、結果として可読性、保守性を下げることになります。これは避けるべきです。

第 10 章:アーキテクチャ全般・ライブラリなど #

通常のユースケースで発生しうる例外 - P.97 #

それぞれのレイヤーの責務に応じたバリデーションを行い、異常があればそのレイヤーで定義している例外を投げます。

ドメイン層では、ドメイン知識(ルール/制約)をチェックし、違反した際にドメイン層で定義する例外を投げます。この場合、ユースケース層はドメイン層から投げられた例外を受け、呼び出し元にどう返すかの判断を行います。ドメイン層の例外に対してそのままエラーで処理を終えるのか、代わりに別の処理を行うのかはユースケースの問題です。その判断を委ねるために、ドメイン層の例外は検査例外とします。(Error ではなく Exception)

ユースケース層では、あらかじめ想定する異常系の処理パターンだった時に、ユースケース層で定義する例外を投げます。ユースケース内の分岐で例外を投げる場合と、前述の通り、ドメイン層の例外をキャッチしてユースケース層の例外に詰め替えて投げる場合があります。

バリデーション内容の重複 - P.98 #

ドメイン層とプレゼンテーション層などで、同じ内容のバリデーションをしたくなる場合があります。この場合、メリットデメリットが一長一短な選択肢があるので、その観点を整理します。

まず、ドメインのルール/制約は重要で、確実に守りたいものです。ドメイン層にこのバリデーションがない場合、「ある処理ではバリデーションされたが、別の処理ではバリデーションを忘れた」ということが発生すると、データの整合性が保証できなくなります。これを回避するためには、ドメイン層のロジックとして実装し、それ以外の層からは整合性を破壊できないようにします。このバリデーションは、他の層のものよりも優先する方が良いでしょう。

一方、同じ内容のバリデーションをプレゼンテーション層で実装したい場合があります。プレゼンテーション層にバリデーションを実装する場合、プレゼンテーション層でルールを明示的に管理できるメリットと、同じ内容のバリデーションがドメイン層と重複し、仕様変更時に修正が漏れるリスクが発生する、というデメリットがあります。

これらのメリットデメリットは一長一短なため、一概にどちらが良いとは決められません。各プロジェクトにおいて、重視するポイントを考慮して決定することになります。

処理結果を例外で表現しない場合 - P.99 #

例外を使用する場合は、例外が投げられればメソッド内の処理が失敗、投げられなければ成功という判断を行います。一方、この表現を Success, Fail といった結果を表すオブジェクトを返す方針もあります。

メリットとしては、前述のような例外の使用が一般的でない言語やフレームワークを使用している時に適用しやすいことです。デメリットとしては、チェックのたびに分岐して return しなければいけないため、例外を使用する場合に比べて呼び出し元の記述が煩雑になることです。

共有されるオブジェクト - P.100 #

複数の箇所で使用するオブジェクトは、該当するレイヤーのパッケージ以下に「shared」などの意図が明確な名前のパッケージを作り、そこにまとめます。

配置するレイヤーをきちんと検討した上で決定することは重要です。例えば、日付を扱う処理に関して、業務的に意味のある日付を処理するオブジェクトであればそれはドメイン層に属するものであり、表示時の日付フォーマットを変換するオブジェクトであればプレゼンテーション層に属するものとなります。レイヤーを意識せずに共有オブジェクトを作成すると、責務が曖昧で凝集度が低く、保守性が低いものになる可能性が高くなります。