ドメインモデル内でリポジトリのメソッドを呼び出すことについて

軽量DDDやクリーンアーキテクチャでコードを書く際、リポジトリ(インターフェースアダプター)の呼び出しはアプリケーションサービス(ユースケース)で行うことが一般的という認識だったが、ドメインモデル(エンティティ)内で呼び出した方がいいケースもあるのでは?とふと思い、これまで読んだ書籍を見返してみたので、そのメモ。

ドメインモデル内で呼び出した方がいいケースの例としては、事前条件によって、呼び出すリポジトリのメソッドが変化するケースが考えられる。 アプリケーションサービスでリポジトリのメソッドを呼び出すようにすると、事前条件をチェックする条件分岐がアプリケーションサービスに含まれることになる。 これは、ドメインロジックがアプリケーションサービスに漏洩していると考えられるケースもあるのではないか。 ここで、ドメインモデル内で事前条件をチェックしリポジトリのメソッドを呼び出すようにすれば、アプリケーションサービスにあった条件分岐がドメインモデルに移動する。 こっちのドメインモデル内でリポジトリのメソッドを呼び出す方がいいケースもあるのでは?とふと思った。

これに関して、今まで読んだ書籍で関連しそうな記述を探してみた。

クリーンアーキテクチャ

クリーンアーキテクチャに記載されている典型的なシナリオでは、ユースケース(アプリケーションサービス)からリポジトリを呼び出している。

図22-2は、データベースを使ったウェブベースのJavaシステムの典型的なシナリオを示している。ウェブサーバーは、ユーザーからの入力データを受け取り、左上のControllerに渡す。Controllerは、プレインオールドなJavaオブジェクトにデータを詰め込み、InputBoundary(ユースケースのインタフェース)を経由して、UseCaseInteractor(ユースケース)に渡す。UseCaseInteractor(ユースケース)は、そのデータを解釈し、Entitiesのダンスを制御する。また、DataAccessInterface(リポジトリのインタフェース)を使用して、Entitiesが使うデータをDatabaseからメモリに持ってくる。それが終わると、UseCaseInteractor(ユースケース)はEntitiesからデータを収集し、OutputDataをプレインオールドなJavaオブジェクトとして生成する。OutputDataは、OutputBoundaryインターフェイスを経由して、Presenterに渡される。

クリーンアーキテクチャ 第22章 クリーンアーキテクチャ 典型的なシナリオ

実践ドメイン駆動設計

実践ドメイン駆動設計では、リポジトリを集約(ドメインモデル)に差し込むことは、一般に好ましくないとしている。

依存性の注入を使ってリポジトリドメインサービスを集約に差し込むことは、一般に好ましくないと考えられている。その目的はおそらく、依存するオブジェクトのインスタンスを集約の内部から扱えるようにすることだろう。依存するオブジェクトは別の集約かもしれないし、ひとつだけではなく複数あるかもしれない。「ルール:他の集約への参照には、その識別子を利用する」で説明したとおり、依存するオブジェクトは事前に探しておいて、集約のコマンドメソッドにそれを渡すほうが好ましい。切り離されたドメインモデルは、一般的にはあまり好ましくない手法だ。

実践ドメイン駆動設計10.8 実装

単体テストの考え方・使い方

自分がまさに悩んでいたことが書いてあった。 この問題については様々なトレードオフを考える必要があるが、本書の結論としては、ドメインモデルに外部依存(リポジトリ)を注入はせず、アプリケーションサービス(本書ではコントローラと表現されている)にて外部依存(リポジトリ)を呼び出すべき、としている。

条件によって振る舞いが異なるロジックを扱う場合、通常、ドメイン層からプロセス外依存を取り除いた状態を維持することは簡単にはできません。そのため、様々なトレードオブについて考えることが求められるようになります。

-- 中略 --

ビジネスロジックのコードと連携を指揮するコードの連携を最も行いやすいのは、ビジネスオペレーションが次の3段階の流れになっている場合です。

・ストレージからのデータの取得

ビジネスロジックの実行

・変更されたデータの保存

 しかしながら、これらの手順を完璧に遵守できない場合もよくあります。たとえば、決定を下す過程の中で、途中で得た結果を使ってプロセス外依存から新たにデータを取得しなければならないような場合です。

-- 中略 --

このときに重要なのは、次の3つの性質がうまくバランスがとれていることです。

ドメインモデルのテストのしやすさ

・コントローラの簡潔さ

・パフォーマンスの高さ

しかしながら、これら3つの性質をすべて揃えることはできず、2つまで然備えることはできません。

・外部依存に対するすべての読み込みと書き込みをビジネスオペレーションのはじめや終わりに持っていくーーこの選択をした場合、コントローラの簡潔さをたもてるようになり、さらに、ドメインモデルをプロセス外依存から隔離した状態を保てるようになる(テストが行いやすくなる)。しかしながら、パフォーマンスが劣化する。

ドメインモデルにプロセス外依存を注入するーーこの選択をした場合、パフォーマンスが高さとコントローラの簡潔さを維持されるようになる。しかしながら、ドメインモデルに対するテストのしやすさは失われることになる。

・決定を下す過程をさらに細かく分割するーーこの選択をした場合、パフォーマンスの高め、テストを行いやすくできるが、コントローラに対して分割した過程を管理するための決定を下す箇所を新たに加える必要がでてくるため、コントローラの簡潔さを保てなくなる。

 ほとんどのソフトウェアにおいて、パフォーマンスの高さは重要な性質なので、1つ目の選択肢(外部依存に対するすべての読み込みと書き込みをビジネスオペレーションのはじめや終わりに持っていくこと)が選ばれることはありません。一方、2つ目の選択肢(ドメインモデルにプロセス外依存を注入すること)はほとんどのコードを過度に複雑なコードに属するようにしてしまいます。まさに、このことが1回目のリファクタリングを必要とする事になった原因なのです。そのため、本書では、ドメインモデルにプロセス外依存を注入することは避けるべきだと考えています。なぜなら、そのような実装にしてしまうと、ビジネスロジックをプロセス外依存とのコミュニケーションから分離することができなくなってしまい、その結果、テストをすることも保守をすることも難しくなってしまうからです。

 こうなると、選べるものは3つ目の選択肢(決定を下す過程をさらに細かく分割すること)だけになります。ただし、この選択肢を選ぶと、コントローラのコードは複雑になってしまい、その実装が過度に複雑なコードの領域に近づくことになります。

単体テストの考え方・使い方 7.4 コントローラにおける条件付きロジックの扱い

結論

どの書籍でもドメインモデルにリポジトリを注入することはNGとしている。 リポジトリをアプリケーションサービスから呼び出すことでアプリケーションサービスのコードの複雑性が高くなってしまうことがあるが、ドメインモデルにリポジトリを注入することのデメリットとのトレードオフを考えると、これは仕方ないこと。