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