レイヤとドメイン駆動設計の関係 | これで理解できるドメイン駆動設計!
Prev: ドメイン駆動設計の戦術的設計とは
レイヤとは何か
ドメイン駆動設計の戦術的設計とはでもレイヤについては言及しました。
レイヤというのは、そのまま層であり階層構造を成すものです。各階層は下の階層あるいは同心円の中心に向かって依存し、その逆方向へは依存することは許さないというだけです。機能的な話は本当にそれだけです。ただ、同じ役割の物を同じレイヤに属させることで、関心の分離を達成するという狙いもあります。
クリーンアーキテクチャやオニオンアーキテクチャが、それ以前と対比して新しい点はドメインレイヤを中心においていることです。それまではドメインレイヤがインフラストラクチャレイヤに依存するのが当たり前だったようです。つまり、インフラストラクチャレイヤがドメインレイヤより下あるいは同心円の中心にあったということです。クリーンアーキテクチャやオニオンアーキテクチャは全く逆で、インフラストラクチャレイヤを同心円の最も外側に追いやっています(正確にはクリーンアーキテクチャの場合、データベースやフレームワークなどもレイヤと認識しているので、外側から二番目になります)。
上記の図をみて、ユースケース(アプリケーション)レイヤとインフラストラクチャレイヤについてドメイン駆動設計の戦術的設計とはでの説明と食い違っていることに気づくかもしれません。つまり、ユースケース(アプリケーション)レイヤがインフラストラクチャレイヤの内側にあり、インフラストラクチャレイヤに依存してはいけないはずなのに、前章の説明ではユースケース(アプリケーション)レイヤはインフラストラクチャレイヤに依存しているという点です。これは、理論上の模範解答と現実的な折り合いを考えた回答の違いです。詳しくは後述します。
ドメイン駆動設計とレイヤパターンの関係
ドメイン駆動設計とクリーンアーキテクチャ、オニオンアーキテクチャは直接的には関係がありません。ドメイン駆動設計で触れられている設計パターンのApplicationServiceがユースケース(アプリケーション)レイヤ、AggregateやEntityがドメインレイヤ、Repositoryがインフラストラクチャレイヤに属するため、どういう階層構造にするべきかという問題があるだけです。
エリック・エヴァンスのドメイン駆動設計ではレイヤードアーキテクチャが紹介されていますが、これは「レイヤードアーキテクチャ」という単一のアーキテクチャではなくレイヤを重ねたアーキテクチャの総称として言及されています。クリーンアーキテクチャもオニオンアーキテクチャも、そういう意味ではレイヤードアーキテクチャの一種です。つまり、ドメイン駆動設計を実践するにあたってレイヤードアーキテクチャを構築するのに、クリーンアーキテクチャやオニオンアーキテクチャが用いられているということです。
各レイヤの解説
ドメインレイヤ
ドメインモデルやドメインロジックに対応するコードが格納されるレイヤです。ドメインモデルやドメインロジックをコードで表現するために、Module(Package)、DomainService、Aggregate、Entity、ValueObject、Specificationの設計パターンが用いられます。レイヤとして最も内側であり、他のレイヤへの依存は許されません。
人によってはDIしていればドメインレイヤがインフラレイヤなどのクラスのインスタンスを受け取るのはありと考えている人がいるようです。しかし、個人的にはドメインレイヤの各クラスはDIを受けることはなく、受け取るのはプリミティブな値や構造体のようなデータのみを保持する型か他のドメインレイヤのクラスのインスタンスのみにした方が良いと考えています。これは、DIしたらドメインレイヤの外部への直接的な依存はなくなるとはいえ、ドメインレイヤの外部に意識を向けることは避けられないというのが理由です。これは本当に個人的な好みなのかもしれませんが、ドメインレイヤはドメインレイヤの中だけ考えれば良いようにすることで、可読性もメンテナンス性も上がると考えています。
クリーンアーキテクチャの理屈においては、UI、データベース、フレームワークに対して非依存でなければいけません。しかし、UI、データベースはともかく、フレームワーク非依存というのは非現実的でしょう。RubyにおいてはActive SupportというRailsに組み込まれたライブラリがあります。これは、Rubyの言語機能を拡張しているライブラリで気づかないうちに利用していることも多々あり、Active Supportに依存せずにコードを書くのは至難の業です。他の言語においても例えば金額計算に特化したライブラリを導入している場合、ドメインレイヤでそのライブラリに依存しないというのは馬鹿げたアイディアであるのはわかるでしょう。
TypeScriptで型をちゃんと定義しようとして時間を浪費することを揶揄して型パズルなどと言われることがありますが、レイヤアーキテクチャにおいてもアーキテクチャの制限に従おうとして時間を浪費してしまうことがあります。
特にクリーンアーキテクチャを「理解」しようとして「実践」しようとするとレイヤパズルにハマり込んでしまうことが多いようです。クリーンアーキテクチャは理論と理屈に基づいて依存に制約を課すとこうなるという理論上の模範解答です。悪い言い方をするなら、机上の空論だということです。現実の仕事に取り組むときには、過剰な制約であったり懸念しているリスクが実現しないために無駄な制約となるものが多いのです。
あくまで、クリーンアーキテクチャやオニオンアーキテクチャは理論上の模範解答であると認識した上で、現実の仕事に取り組む時は現実と折り合いをつけることが重要になります。
インフラストラクチャレイヤ
外部のデータベースやAPIなどへのアクセスを担う処理が格納されるレイヤです。Repository、Factory、Gatewayが格納されます。
クリーンアーキテクチャやオニオンアーキテクチャにおいては一番外側にあるレイヤです。しかし、通常インフラストラクチャレイヤはユースケース(アプリケーション)レイヤで使われます。このため、クリーンアーキテクチャやオニオンアーキテクチャにおいては、ユースケース(アプリケーション)レイヤはインフラストラクチャレイヤのクラスに直接依存せずDIによって間接的に依存する形をとっています。
これは、ユースケース(アプリケーション)レイヤはインフラストラクチャレイヤの変更の影響を受けるべきではないという考え方に基づくものです。ユースケース(アプリケーション)レイヤがインフラストラクチャレイヤに直接依存している状態で、例えばMySQLからMongoDBへ、あるいはマイクロサービス化してAPIを叩くRepositoryに切り替えることになったら、ユースケース(アプリケーション)レイヤにも手を入れる必要が出てくるかもしれません。それをDIすることで影響範囲をインフラストラクチャレイヤに止めることができるというわけです。
馬鹿げた話だとお思いでしょうか? はい、実際に馬鹿げた話なのです。杞憂というやつです。大体の場合、ユースケース(アプリケーション)レイヤに対してインフラストラクチャレイヤのクラスをDIする必要性はありません。データベースなどの永続化先が変わることなんて、まずありません。起こりもしないことに気をかけて、実装コストや可読性を犠牲にするのは愚かな選択です。
DIすることでインフラストラクチャレイヤに依存せずに単体テストが行えるようになるという側面もあります。しかし、ユースケース(アプリケーション)レイヤを単体テストする必要性はありません。ビジネスロジックはドメインレイヤに書かれているはずで、ユースケース(アプリケーション)レイヤにあるのはドメインレイヤとインフラストラクチャレイヤを繋ぐグルーコードだけです。単体テストする価値のあるコードがそもそもないのです。であるならば、インフラストラクチャレイヤも含めた結合テストだけを行うのが理にかなっています。
唯一、DIすることが理にかなっているのは、外部サービスのAPIを叩くインフラストラクチャレイヤのクラスなどがある場合のみです。テストコードを実行するたびに外部APIを叩くわけにはいかないですから、このインフラストラクチャレイヤのクラスはDIして、モックをインジェクションできるようにしておく必要があります。
DIする場合にはRepositoryのinterfaceを定義する必要があります。Repository本体が実装するのはもちろん、テストで定義するRepositoryMockもそのinterfaceを実装する必要があります。ApplicationServiceにとってはDIによって受け取るインスタンスの型としてRepositoryのinterfaceが必要になります。問題となるのはinterfaceの格納場所です。Repositoryの場合はAggregateと対になるという点とクリーンアーキテクチャなどの依存方向の制限からドメインレイヤに格納されることが多いようです。しかし、DomainModelと無関係なGatewayを考えるとドメインレイヤにインフラストラクチャレイヤのinterfaceを格納するよりかはインフラストラクチャレイヤにRepositoryやGatewayのinterfaceを格納してしまう方が良いだろうと思います。ただ、レイヤの依存関係に違反することにはなるので、個人の好みが別れるところかとは思います。
なお、このinterfaceの話は静的型付け言語に限った話です。Rubyのようなダックタイピングな言語ならコード上には表出しません。しかし、どういう操作ができるインスタンスを受け取る想定をしているのかはきちんと考え、必要ならばコメントに残すなどの対応が必要でしょう。
アーキテクチャ上の意思決定は保険を契約することに似ています。保険は懸念されるリスクが実現したとき、保険会社にリスク対処のコストを移転させることを目的に契約されます。アーキテクチャ上の意思決定は懸念されるリスクが実現する可能性を抑えたり、リスクが実現したときに影響を小さくすることを目的に判断されます。
つまり、アーキテクチャ上の意思決定も保険の契約も、どちらもリスクマネジメントの一環として行われるのです。どちらも問題となるポイントは同じです。
- リスクの実現可能性
- リスクが実現した時に被る被害の大きさ
- リスク対処のために使うコスト量
- リスク対処によって享受できるメリット量
リスクの実現可能性がゼロのリスクに保険を掛ける人はいません。保険料が無駄になるだけですからね。
リスクが実現した時に被る被害がほとんどない場合も保険を掛ける人はいないでしょう。保険料の方が高くつくことになりかねません。
リスク対処のために使うコストが非常に大きい場合も保険を掛ける人はいないでしょう。莫大な保険料が必要なら別のことに使った方が良いでしょう。
リスク対処によって享受できるメリット量が小さい場合も保険を掛ける人はいないでしょう。これも保険料の方が高くつくことになりかねません。
ところが困ったことにアーキテクチャ上の意思決定となると、リスクの内容とリスク対処によって得られると言われているメリットだけ考えて判断されることが多いようです。「入院したら最高5000万円」という文言だけ見て5000万円もらえると思って保険料も確認せずに契約するようなことを行なっているのです。アーキテクチャ上の意思決定においても、上記の4項目を考えリスクが実現する可能性があるのか、リスクを受容できないほどの被害が想定されるのか、リスク対処のコストが現実的か、リスク対処法がリスク対処として機能するのか考えて判断しなければなりません。
ユースケース(アプリケーション)レイヤ
アプリケーションとしてのエンドポイントとして機能するクラスが格納されるレイヤです。ApplicationServiceが格納されます。
例えば「求職者が履歴書を登録する」とか「企業担当者が応募者の情報を閲覧する」、「管理者が規約違反ユーザーを凍結する」と言ったユースケースやユーザーストーリーに対応するApplicationServiceが格納され、アプリケーションとしてのユースケースを提供するレイヤです。rails consoleのようなREPL、Controller、バッチスクリプトから、それらのApplicationServiceが呼ばれることになります。
ユースケースに対応するApplicationServiceを実装するため、ApplicationService単体でトランザクションが完結することになります。全体として結果整合性を成すとしても、ApplicationServiceは原子性を持つと考えるべきです。
インフラストラクチャレイヤをDIするべきかどうかは、インフラストラクチャレイヤの節で書いた通りです。
レイヤをまたぐ
Clean Architecture 達人に学ぶソフトウェアの構造と設計においては、緩いレイヤードアーキテクチャと呼ばれる形、例えばユースケース(アプリケーション)レイヤを飛ばしてControllerから直接Repositoryを利用するような形は望ましくないと書かれています(厳密にはCQRSの場合ならControllerからRepositoryを利用するのはありとか書かれているが、どういったことを想定しているのか私にはよくわからなかった。ログイン中のユーザーをControllerで参照するようなケースを想定しているのかもしれない)。
しかし、現実問題としてはレイヤを省略することはありだろうと思っています。これはアプリケーション全体にわたるレイヤの話ではなく、個別のApplicationServiceごとにレイヤのありなしを変えるという話です。以下のような組み合わせが考えられます。フレームワークはRailsを例とします。
- Controllerのみ
- Fat Controllerスタイルだが、単純なCRUDの場合だけ適用するケース
- Controllerから直接ActiveRecordを呼ぶ
- 全レイヤ省略
- Controller、ActiveRecord
- 古典的なRails Wayの実装のケース
- Controllerから直接ActiveRecordを呼ぶがビジネスロジックはActiveRecordのメソッドとして書く
- ドメイン駆動設計を行うなら基本的に避けて、後続の3ケースのいずれかにする
- 全レイヤ省略
- Controller、ApplicationService
- ApplicationServiceからActiveRecordを使うトランザクションスクリプトパターンのケース
- ドメインレイヤ、インフラストラクチャレイヤ省略
- Controller、ApplicationService、Gateway
- ApplicationServiceからGatewayを使うトランザクションスクリプトパターンのケース
- ActiveRecordはGatewayから利用
- ドメインレイヤ省略
- Controller、ApplicationService、DomainModel
- ApplicationServiceがRepositoryやFactoryとしての役割も担ってDomainModelを利用するケース
- インフラストラクチャレイヤ省略
- Controller、ApplicationService、DomainModel、Repository
- ActiveRecordの標準メソッド(whereやアソシエーションメソッドなど)はRepository内だけで利用するケース
- 全レイヤを実装するケース
※DomainServiceとFactoryは必要なら追加する
繰り返しますが、全部の機能で同じレイヤ構成にする必要はありません。この機能は「Controller、ApplicationService」のセット、こっちの機能は「Controller、ApplicationService、DomainModel、Repository」のセットというふうに機能ごとにレイヤ構成を変えるべきです。すべての機能を同じレイヤ構成にしようと考えるのは、すべての機能が同程度の複雑度を持つはずだという非現実的な前提に基づくものです。たとえばECサイトで「ユーザーがパスワードを変更する」機能と「ユーザーが商品を注文する(注文確定、在庫の確保、決済などの処理を含む)」機能が同程度の複雑度と仮定するのは馬鹿げているでしょう。
しかし、それでもApplicationServiceを省略することは避けた方が良いと考えています。繰り返しになりますが、ユースケース(アプリケーション)レイヤはアプリケーションのエンドポイントとなるところです。ApplicationServiceがあるということは、アプリケーションとしてその機能があるという宣言になりますし、REPLからの実処理に則ったテストデータ投入や動作確認が容易になるという利点があります。
Next: ドメイン駆動設計の核心