自分が現状気に入っているアプリケーションのパッケージ構成をさらす


クリーンアーキテクチャや DDD の戦術的設計、CQRS などを勉強して、現状自分が気に入っているアプリケーションのパッケージ構成をさらします。

実際に Java (Spring Boot) 実装してみて、自分としてはある程度納得感を持てた構成になります。

全体像

パッケージ構成の全体像は下図の通りです。

ディレクトリで表現すると以下のようになります。

.
├── application
│   ├── external
│   ├── query
│   │   └── user
│   │       ├── UserQueryModel
│   │       └── UserQueryService
│   └── service
│       └── user
│           ├── UserApplicationService
│           └── UserGetOutput
├── domain
│   └── model
│       └── user
│           ├── User
│           └── UserRepository
├── infrastructure
│   ├── external
│   ├── mysqlrepository
│   │   └── user
│   │       └── UserMySQLRepository
│   └── mysqlquery
│       └── user
│           └── UserMySQLQueryService
└── interfaces
    └── api
        └── user
            ├── UserController
            ├── UserPostRequestBody
            └── UserGetResponseBody

以下の点を考えた結果、ここに至りました。

  • インターフェース層、アプリケーション層、ドメイン層、インフラストラクチャ層の 4 層にする
  • クリーンアーキテクチャなどで示されているように、インフラストラクチャ層にドメイン層やアプリケーション層が依存しない構成にする
  • Repository パターンによる、集約単位でのデータアクセスを採用する
  • Repository で表現すべきでない複雑なデータアクセスは QueryService への切り出す

各パッケージの役割

domain.model

ドメイン層です。domain ではなく domain.model というパッケージ名にしているのは、IDDD 本などを参考にしています。
また、ドメイン層で投げる例外を定義する場合などは、domain.exception などのパッケージを切ったりします。

この層にはいわゆるドメインモデルを配置しますが、Entity、ValueObject などを含む Aggregate (集約) だけを図に示しています。

データアクセスのインターフェースである Repository もこの層に配置しています。
Repository のインターフェースはアプリケーション層に配置する例もあると思いますが、自分はドメイン層に配置する派です。
理由としては、ドメイン層に配置したほうが「Entity ごとではなく集約ごとに Repository を実装する」ということを意識しやすいからです。

domain.model 配下には user などのディレクトリをさらに切った上で各種モデルを配置します。
user などのディレクトリは集約を意識しつつ、適宜分類しています。

application

アプリケーション層です。
一言で言うと、ユースケースの流れを実現する層です。

application.service

アプリケーションサービスを配置します。
アプリケーションサービスは、ドメインモデルなどの操作によるユースケースの実現する役割を持ちます。
また、トランザクション管理も担います。

アプリケーションサービスは、クリーンアーキテクチャで UseCase と呼ばれるものと同じ認識です。

むやみにデータの詰め替えコードが増えないよう、戻り値としてドメインモデルをそのまま返したり、Output 型の属性としてドメインモデルを使うことも許容しています。

引数としては

  1. ValueObject など、ドメインモデルの型を複数受け取る
  2. int、String など、言語が提供する型を複数受け取る
  3. 引数専用の Input 型を作る (属性は ValueObject などのドメインモデルの型)
  4. 引数専用の Input 型を作る (属性は int、String などの言語が提供する型)

といった方法がありますが、私は「型安全ではありたい。Input 用のクラスを作るのは面倒」という理由で、「1. ValueObject など、ドメインモデルの型を複数受け取る」を採用することが多いです。

application.query

Repository パターンでは実現しにくいデータアクセスのインターフェースを配置します。
戻り値は適宜 QueryModel といった型を作成して取得します。

なぜ Repository だけでなく QueryService が必要になるかは、「CQRS実践入門 [ドメイン駆動設計]」という記事が今まで見た中で最も分かりやすいです。

一言で言うと、集約単位でしかデータアクセスを扱えない Repository パターンのデメリットを補うためです。

application.external

外部連携のインターフェースもアプリケーション層に配置します。
インターフェースの引数や戻り値としては、独自に型を定義したり、ドメインモデルの型を使ったりします。

interfaces.api

インターフェース層です。
この図では API として実装する想定で、api というディレクトリを切っています。
コマンドラインアプリケーションであれば interfaces.cmd のようなディレクトリを作成します。

ここには Controller や、RequestBody・ResponseBody などの型を表す DTO を配置します。
Web アプリケーションの場合、HTTP に依存する処理などはこの層だけに記述し、アプリケーション層などには見せません。

上図では表現していませんが、詰め替えロジックが過度に増えないよう、インターフェース層がドメインモデルを参照することは許容しています。

※ interface が複数形の interfaces になっているのは予約語との競合を避けるためです。

infrastructure

インフラストラクチャ層です。
実装の詳細を配置します。

infrastructure.mysqlrepository

Repository の実装を配置します。
Repository パターンに従い、集約単位でのシンプルな CRUD のみを実施します。

読み込み系では、集約単位で必要なデータをすべて取得し、集約の整合性を保証します。
そのため、例えば SQL であれば、SELECT・FROM・JOIN 句が基本的にすべての参照系クエリで使えまわせるはずです。

infrastructure.mysqlquery

QueryService の実装を配置します。
Repository が集約単位でしかデータアクセスを扱えないため、集約単位で扱いたくないデータアクセスを QueryService で実施します。

infrastructure.external

外部連携の実装を配置します。
外部連携の RequestBody、ResponseBody などはこの層に隠蔽し、いわゆる腐敗防止層の役割も果たします。

その他

インフラストラクチャ層には、フレームワークなどの設定も配置します。
その際は、例えば infrastructure.springframework のようなパッケージを切ります。

悩んでいるところ

Repository をドメイン層で呼び出していいか

Repository は実体としてはデータアクセスであり、ある程度コストのかかる処理になります。
それをアプリケーション層から直接見えない場所から呼び出してしまうと、1 つのユースケースの処理でどれだけデータアクセスしているかを把握しにくくなるのではないかと懸念しています。

私は基本的には Repository は ApplicationService のみから呼び出し、場合によっては DomainService から呼び出すことも許容するくらいに考えています。

Repository をドメイン層に配置すべきか

「Entity ごとではなく集約ごとに Repository を実装する」ということを意識しやすくするため、Repository はドメイン層に配置しています。
しかし、上記のように、ドメイン層で気軽に Repository を呼びすぎないようにするため、ドメイン層ではなくアプリケーション層に配置すべきかもしれません。

ApplicationService の処理の共通化

ApplicationService 間で処理を共通化したいときにどうするかは考え中です。
今のところ、他の ApplicationService の処理を呼ぶことを許容する場合と、XxxService のようなクラスを作ってしまう場合があります。

QueryService をどれだけ使うか

QueryService は集約同士をアプリケーション上で JOIN することを防ぐなどして、コードの可読性やパフォーマンス向上などに寄与してくれます。
しかし、QueryService には DB に変更が生じた場合の影響範囲が広くなるといったデメリットもあります。

そのため、私はあえてアプリケーション上で集約同士を JOIN してしまう場合もあります。
パフォーマンス上のネックが発生した場合や、より特化したクエリを発行したい場合など、最小限だけ QueryService を使うことにしています。

外部連携

外部連携のインターフェースをアプリケーション層に置くべきか、ドメイン層に置くべきかも悩んでいます。
この例では、外部連携であることをアプリケーション層で意識するように、という理由でアプリケーション層に配置しています。

おわりに

自分はこの構成である程度納得感を持って実装できるようになってきました。
しかし、今後もさらにブラッシュアップされていくことかと思います。

また、DB を分離するレベルの CQRS やイベントソーシングを取り入れた場合、フロントエンドで Flux と組み合わせた場合など、より高度な構成でどうするかは考え中です。

このような構成を考えるのは非常に楽しいので、またアウトプットできるよう精進していきます。

参考

書籍

Web