きっかけ
ドメイン駆動開発について社内で少し議論した際に、曖昧な部分が残っていること、自分なりにリポジトリ構成がどうあるべきかをちゃんと考えられていなかったことに気付いたため、自分なりに納得のいく構成を整理してみようと思った。
全体概要
クリーンアーキテクチャの考え方でまずは考える。
円を上下並びに置き換えて捉えると、下記理解。
シンプルなアプリケーション構成の場合
全体構成
全体概要をもう少し実装ベースで整理すると、下記想定。
リポジトリ構成
presentation/
┃┣━middleware/
┃┗━controllers/
application/
┃┗━usecases/
┃ ┗━aaaUsecase
domain/
┃┗━xxxDomain個別ドメイン/
┃ ┣━xxxDomain
┃ ┗━xxxDomainRepository(IF)
infrastructure/
┣━repositories/
┃ ┗━xxxDomainRepositoryImpl
┣━database/
┗━api/
各クラスの役割
- middleware
- 認可処理やログ設定など
- controllers
- ルーティング
- 入力項目の型・桁数のチェック(ユースケースが実行可能かどうかだけチェック。それ以上のチェックはユースケース側で行うので、コントローラ側では行わない)
- ユースケース実行
- 出力データ編集(ユースケースの結果を必要に応じてカスタマイズ)
- usecase
- 業務処理を実行
- データ参照・更新が必要な場合、リポジトリを介して行う
- ドメイン操作において業務ルールに違反している場合、エラーを返す
- トランザクション管理
- 業務処理を実行
- domain
- 各domain
- 業務モデルを定義
- 業務ルールを定義
- 各repository(IF)
- 紐づくdomainのデータ永続化(CRUD)を抽象化
- 各domain
- infrastructure
- repositories
- repository(IF)を実装する、具体的なデータ永続化実装
- database
- DB接続関連実装
- api
- 各外部APIの送受信関連実装
- repositories
システムが複雑化してきた時に発生する課題
repositoryがデータ永続化を全て担う場合、システムが複雑化してくるとデータ読み取り関連のパターンが増えてきたり、特定項目のデータ参照のためだけにドメイン全体の取得が必要になって通信コストが肥大化するなどの問題が発生するようになる。
<例① データ参照関連のパターンが増加してrepositoryIFが汚くなっていく>
save
delete
findActiveUsers
findInactiveUsers
findUsersByRole
findUsersByRoleAndStatus
findUsersByRoleAndStatusAndArea
find...
……または……
findBy( <- ひとつのメソッドにまとめると、今度は引数が肥大化
isActive,
role,
status,
area,
……
)
<例② 特定項目のデータ参照のためだけにドメイン全体の取得が必要になって通信コストが肥大>
id
name <- この項目だけ参照したい場合でも、他の項目も取得対象になる
createdAt
updatedAt
condition <- 例えばこの項目がDBでは別テーブル管理や外部API経由だと、通信コストが増加する
解決方法として、Specificationを活用するパターンやCQRSを導入するパターンがある。
複雑なアプリケーション構成の場合
Specificationを活用するパターンとCQRSを導入するパターン、どちらでもやりたいことは変わらず、データ読み取り関連の処理最適化。
ardalis氏のSpecificationの考え方はよりDDDに寄っており、データ永続化に関わる処理をRepositoryで完結させることができる点は非常に魅力的(Evans氏が提唱するSpecificationとは別の考え方なので、あくまで"寄っている"もの)。
ただ、残念ながらこのSpecificationは.NET系かJava・Kotlinとかじゃないと言語仕様的に実現が難しい。なのでそれ以外の言語を採用する場合はCQRSを採用する方がよい。
Specificationパターンの場合
presentation/
┃┣━middleware/
┃┗━controllers/
application/
┃┗━usecases/
┃ ┣━aaaUsecase
┃ ┣━aaaDto
┃ ┗━aaaSpec
domain/
┃┗━xxxDomain個別ドメイン/
┃ ┣━xxxDomain
┃ ┗━xxxDomainRepository(IF)
infrastructure/
┣━repositories/
┃ ┗━xxxDomainRepositoryImpl
┣━database/
┗━api/
CQRSパターンの場合
presentation/
┃┣━middleware/
┃┗━controllers/
application/
┃┣━usecases/
┃┃ ┗━aaaUsecase
┃┗━queries/
┃ ┗━bbbQueryService
domain/
┃┗━xxxDomain個別ドメイン/
┃ ┣━xxxDomain
┃ ┗━xxxDomainRepository(IF)
infrastructure/
┣━repositories/
┃ ┗━xxxDomainRepositoryImpl
┣━queryServices/
┃ ┗━bbbQueryServiceImpl
┣━database/
┗━api/
ドメインイベントを採用したいアプリケーション構成の場合
ドメインイベントを定義し、イベントを購読する形を採用したい場合、ドメインイベント・イベントハンドラ・イベントバス(イベントハンドラが複数ある場合のまとめクラス)などを追加する必要がある。
また、同一アプリケーション内でのイベント購読のほか、SQSなどを通じた外部サービスに対するイベント配信の形もある。
リポジトリ構成
presentation/
┃┣━middleware/
┃┗━controllers/
application/
┃┣━usecases/
┃┃ ┗━aaaUsecase
┃┣━queries/
┃┃ ┗━bbbQueryService(IF)
┃┗━events/
┃ ┣━eventBus
┃ ┗━cccEventHandler
domain/
┃┗━xxxDomain個別ドメイン/
┃ ┣━xxxDomain
┃ ┣━xxxDomainRepository(IF)
┃ ┗━xxxDomainEvent
infrastructure/
┣━repositories/
┃ ┗━xxxDomainRepositoryImpl
┣━queryServices/
┃ ┗━bbbQueryServiceImpl
┣━database/
┗━api/
┗━events/
┣━externalEventBus
┗━externalEventHandler
マルチプロジェクト構成
マルチプロジェクト構成にするなら、下記のような構成が良さそう。
各構成要素がCoreに対して依存関係を持つ形で、全体概要の依存関係をきちんと維持できる。
<Application>
presentationのみ担うため、presentationフォルダを不要とする。
middleware/
controllers/
<Batch>※Coreを共有利用するなら
entrypoint
process/
<Core>
applicationとdomainの2層を持つため、該当フォルダを残す形式とする。
application/
┃┣━usecases/
┃┃ ┗━aaaUsecase
┃┣━queries/
┃┃ ┗━bbbQueryService(IF)
┃┗━events/
┃ ┣━eventBus
┃ ┗━cccEventHandler
domain/
┗━xxxDomain個別ドメイン/
┣━xxxDomain
┣━xxxDomainRepository(IF)
┗━xxxDomainEvent
<Infrastructure>
infrastructureのみ担うため、infrastructureフォルダを不要とする。
repositories/
┃┗━xxxDomainRepositoryImpl
queryServices/
┃┗━bbbQueryServiceImpl
database/
api/
events/
┣━externalEventBus
┗━externalEventHandler
もしVertical Slice Architectureに寄せるなら
厳密にusecaseで縦割りするのは規模大きくなると辛いので、features-first寄りな構成を持ち込む。
シングルプロジェクトの場合
features/
┃┣━[feature]/
┃┃ ┗━[usecase]/
┃┃ ┣━controller
┃┃ ┣━usecase
┃┃ ┗━query(直接実装)
events/
┃┣━eventBus
┃┗━cccEventHandler
domain/
┃┗━xxxDomain個別ドメイン/
┃ ┣━xxxDomain
┃ ┣━xxxDomainRepository(IF)
┃ ┗━xxxDomainEvent
infrastructure/
┣━repositories/
┃ ┗━xxxDomainRepositoryImpl
┣━database/
┗━api/
┗━events/
┣━externalEventBus
┗━externalEventHandler
マルチプロジェクトの場合
2分割パターン。
<Application>
features/
┃┣━[feature]/
┃┃ ┗━[usecase]/
┃┃ ┣━controller
┃┃ ┗━usecase
┃┃ ┗━query(IF)
events/
┃┣━eventBus
┃┗━cccEventHandler
domain/
┗━xxxDomain個別ドメイン/
┣━xxxDomain
┣━xxxDomainRepository(IF)
┗━xxxDomainEvent
<Infrastructure>
repositories/
┃┗━xxxDomainRepositoryImpl
queries/
┃┗━bbbQueryImpl
database/
api/
events/
┣━externalEventBus
┗━externalEventHandler
3分割にすると、依存関係はクリーンになるが、features-firstにする意味が薄れる。
<Application>
controllers/
middleware/
<Core>
features/
┃┣━[feature]/
┃┃ ┗━[usecase]/
┃┃ ┗━usecase
┃┃ ┗━query(IF)
events/
┃┣━eventBus
┃┗━cccEventHandler
domain/
┗━xxxDomain個別ドメイン/
┣━xxxDomain
┣━xxxDomainRepository(IF)
┗━xxxDomainEvent
<Infrastructure>
repositories/
┃┗━xxxDomainRepositoryImpl
queries/
┃┗━bbbQueryImpl
database/
api/
events/
┣━externalEventBus
┗━externalEventHandler
なので、features-firstを採用するならシングルプロジェクトがよさそう。
逆にCoreを共有したいならクリーンアーキテクチャ構成のがしっくりくると感じた。

