パッケージ構成に関する戦略
LT;DR
前提として、どのようなプロジェクトにも適合する完璧なパッケージ構成は存在しない
また、どんなパッケージ構成であっても、維持するにはプロジェクトに 規律 を課す必要がある アーキテクチャの構成要素を意識したパッケージ構成 を採用することで、この問題を回避できる
実現するには、すべての層への依存を持つアプリケーションの構成を管理するコンポーネントを用意する
これにより、アプリケーション層は 「実際に渡された実装クラスが何なのか」を知らなくても良くなる
hr.icon
前提
どのようなプロジェクトにも適合する完璧なパッケージ構成は存在しない
どんなパッケージ構成でも、維持するにはプロジェクトに 規律 を課さなければならない サンプルとして、BuckPal というオンラインで送金を行う Web アプリケーションを考える
層を意識したパッケージ構成
code:text
buckpal/
domain/
Account
Acticity
AccountRepository
AccountService
persistence/
AccountRepositoryImpl
web/
AccountController
説明
AccountRepository インタフェースをドメイン層に、その実装を永続化層に配置
問題点
アプリケーション内の各機能に境界を設けられない
パッケージ内が大量のクラスで溢れ、関係ない機能同士が意図せず依存関係を持つ危険性がある
アプリケーションがどのような ユースケース を提供しているのかがパッケージ構成を見ただけで判断できない 特定の機能に関するコードを見つけるためには、どのサービスが対象の機能を持っているか推測し、該当するメソッドが含まれているかを探す必要がある
パッケージ構成を見ても、どの アーキテクチャ を採用したのか分からない Web アダプタと永続化アダプを見つけるために、web と persistence パッケージを探索する しかし、受信・送信ポートはコードの中に隠れているので、パッケージ構成を見ただけでは Web アダプタがどの機能を呼び出しているのか、永続化アダプタがドメイン層に対してどのような機能を提供しているのか知ることができない
機能を意識したパッケージ構成
https://scrapbox.io/files/66be99ca385dcd001d348a5b.png
code:text
buckpal/
account/
Account
SendMoneyController
AccountRepository
AccountRepositoryImpl
SendMoneyService
「層を意識したパッケージ構成」からの変更点
層をすべて取り除き、口座に関するすべてのコードを account に集めている
新しい機能を追加する場合、新しいパッケージを account と同じ階層に作成する
機能間の境界を強固なものにするため、同じパッケージ内でしか使われないクラスの 可視性 はプライベートにする クラスの責務がクラス名から認識できるように名前を変更している
問題点
やっぱり、どのアーキテクチャを採用したのか分からない
どこに アダプタ があるのかパッケージ名から見つけ出せない account パッケージ内はアクセス制限がないため
アーキテクチャの構成要素を意識したパッケージ構成
これらをパッケージ構成に落とし込む
code:text
buckpal/
adapter/
in/
web/SendMoneyController
out/
persistence/
AccountPersistenceAdapter
SpringDataAccountRepository
application/
domain/
model/Account
service/SendMoneyService
port/
in/SendMoneyUseCase
out/UpdateAccountStatePort
common/
adapter パッケージ
Web アプリケーションなので、Web アダプタと永続化アダプタを用意している
application パッケージ: 六角形の外側に相当
warning.icon 依存の向きに関するルールが破られている
common パッケージ: コードベース の至るところで共有されるコード ユーティリティやヘルパ
メリット
「アーキテクチャは単なる抽象的な概念でしかなく、コードと直接結びつかない」ということ
パッケージ構成がアーキテクチャを反映しなくなると、時間の経過とともにコードベースはアーキテクチャからかけ離れたものになる
開発者がアーキテクチャについて 能動的 に考えるようになる パッケージの数が多くなると、現在扱っているコードをどのパッケージに収めるべきか考える必要が出てくるため
懸念点
別のパッケージからアクセスできるようにするために、すべてのクラスを public にする必要がある?
adapter パッケージに関しては、基本的に公開する必要はない
外部からほとんど呼び出されることはないため
ポートを介して間接的に呼び出されるのは除く
application パッケージに関しては、公開しなければならないクラスやインタフェースが存在する
同様に、ドメインモデル もサービスやアダプタからアクセスできるようにする必要がある ただし、ドメインサービス は 受信ポートのインタフェースに隠れているため、private で良い どうしても public にしないといけないケースもある
その場合、予期せぬアクセスを防ぐ方法が必要となる
複数のドメインをどう扱うのか?
domain をサブパッケージに分割する選択も取れるが、これに伴いポートやアダプタもパッケージ分割するのは止めたほうが良い
依存注入の (DI) の役割
https://scrapbox.io/files/66bee59f653c16001c61661f.png
このとき、アプリケーション層内で 送信ポート のインスタンス化を行うと、アプリケーション層にアダプタ層の依存を持ち込んでしまう これを実現するためには?
アプリケーションの構成を管理するコンポーネントを用意する
そのコンポーネントにすべての層への依存を持たせる
アーキテクチャを構成するのに必要なほとんどすべてのクラスのインスタンス化を任せるようにする
上記のコンポーネント図について考える
SendMoneyController と SendMoneyService、AccountPersistenceAdapter に依存性の注入を行う、DI コンテナを準備する
SendMoneyController
コンストラクタは引数で SendMoneyUseCase を受け取るようになっている
なので、DI コンテナ は SendMoneyUseCase インスタンスを生成し、SendMoneyController コンストラクタに渡す
SendMoneyService についても同様
コンストラクタは引数で UpdateAccountStatePort を受け取るようになっている
なので、DI コンテナ UpdateAccountStatePort インスタンスを生成し、SendMoneyServiceコンストラクタに渡す
これにより、SendMoneyController と SendMoneyService、AccountPersistenceAdapter クラスは「実際に渡された実装クラスが何なのか」を知らなくても良くなる