TL;DR
- どのプロジェクトにも合う完璧なパッケージ構成は存在せず、どの構成でも維持するには規律が必要である。これを守れないとアーキテクチャ対コード間ギャップが生じる。アーキテクチャの構成要素を意識したパッケージ構成にすれば、この問題を避けられる。
- 送信アダプタは依存の向きを処理の流れと逆にする必要がある。アプリケーション層が送信ポートの実装を自分で生成するとアダプタ層への依存が生じるので、依存性の注入で解決する。
内容
前提
どのプロジェクトにも合う完璧なパッケージ構成は存在しない。どのパッケージ構成でも、維持するにはプロジェクトに規律を課す必要がある。この章では、オンラインで送金する Web アプリケーション BuckPal を例に、3 つの構成を比べる。
層を意識したパッケージ構成
buckpal/
domain/
Account
Activity
AccountRepository
AccountService
persistence/
AccountRepositoryImpl
web/
AccountControllerWeb 層、ドメイン層、永続化層の 3 つの層ごとにパッケージを用意する。依存関係逆転の原則を使い、AccountRepository インタフェースをドメイン層に、その実装を永続化層に置いて、依存の向きをドメイン層に向けている。
この構成には 3 つの問題がある。1 つ目は、機能ごとに境界を設けられないことである。パッケージが大量のクラスであふれ、関係のない機能同士が意図せず依存する危険がある。2 つ目は、アプリケーションがどんなユースケースを提供しているかがパッケージ構成から分からないことである。ある機能のコードを探すには、どのサービスが担当しているかを推測し、メソッドを探す必要がある。3 つ目は、どのアーキテクチャを採用したかが分からないことである。ヘキサゴナルアーキテクチャだと推測して web と persistence を見ても、受信ポートと送信ポートはコードの中に隠れている。そのため、Web アダプタがどの機能を呼んでいるのか、永続化アダプタがドメイン層に何を提供しているのかが分からない。
機能を意識したパッケージ構成
buckpal/
account/
Account
SendMoneyController
AccountRepository
AccountRepositoryImpl
SendMoneyService層をすべて取り除き、口座に関するコードを account にまとめる。新しい機能は、account と同じ階層に新しいパッケージとして追加する。機能間の境界を強くするため、同じパッケージ内でしか使わないクラスはパッケージプライベートにする。クラス名も、責務が名前から分かるように変える(叫ぶアーキテクチャ)。
しかし、この構成でもどのアーキテクチャを採用したかは分からない。アダプタやポートがどこにあるかをパッケージ名から見つけられない。また、account パッケージ内にはアクセス制限がないので、依存関係逆転の原則を適用しても、ドメイン層のコードが永続化層のコードに依存するのを防げない。
アーキテクチャの構成要素を意識したパッケージ構成
ヘキサゴナルアーキテクチャの主な構成要素は、エンティティ、ユースケース、ポートとアダプタである。これをパッケージ構成に落とし込む。
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 にはドメインモデルを置き、エンティティを調整する役割もここで担う(ただし、これは依存の向きのルールを破っている。手を動かしてわかるクリーンアーキテクチャ 第3章 依存関係の逆転)。port にはポートのインタフェースを置き、依存関係逆転の原則を適用するために application に含める。common には、ユーティリティやヘルパのようにコードベース全体で共有するコードを置く。
この構成のメリットは 2 つある。1 つは、アーキテクチャ対コード間ギャップ(モデル対コード間ギャップ)を解消できることである。アーキテクチャは抽象的な概念でしかなく、コードと直接結びつかない。パッケージ構成がアーキテクチャを反映しなくなると、時間とともにコードベースはアーキテクチャからかけ離れていく。もう 1 つは、パッケージが多いので、今書いているコードをどこに置くかを考える必要があり、開発者がアーキテクチャについて能動的に考えるようになることである。
すべてのクラスを public にする必要があるのかという懸念もある。adapter パッケージは、ポートを介して間接的に呼ばれるほかは外部から呼ばれないので、基本的に公開しなくてよい。これでアプリケーション層から誤って依存されるのも防げる。application パッケージでは、アダプタから使うポートのインタフェースや、サービスやアダプタから使うドメインモデルは公開する必要がある。ドメインサービスは受信ポートのインタフェースの裏に隠れるので、非公開でよい。どうしても公開しなければならない場合は、予期しないアクセスを防ぐ方法が必要になる(第 12 章「アーキテクチャ内の境界の維持」)。
複数のドメインをどう扱うかについては、ヘキサゴナルアーキテクチャでは特に言及されていない。domain をサブパッケージに分けることはできるが、それに合わせてポートやアダプタまで分けるのはやめたほうがよい(第 13 章「複数の境界づけられたコンテキストの管理」)。
依存性の注入の役割
送信アダプタでは、依存関係逆転の原則を使って依存の向きを処理の流れと逆にする。アプリケーション層にインタフェース(送信ポート)を置き、アダプタ層にそれを実装したクラス(送信アダプタ)を置く。このとき、アプリケーション層で送信ポートの実装をインスタンス化すると、アプリケーション層にアダプタ層への依存を持ち込んでしまう。
これを防ぐのが依存性の注入である。アプリケーションの構成を管理するコンポーネント(DI コンテナやメインコンポーネント)を用意し、そのコンポーネントにすべての層への依存を持たせて、ほとんどすべてのクラスのインスタンス化を任せる。たとえば SendMoneyController のコンストラクタは SendMoneyUseCase を受け取るので、DI コンテナがそのインスタンスを生成して渡す。SendMoneyService も同様に、UpdateAccountStatePort を受け取る。こうすると、SendMoneyController、SendMoneyService、AccountPersistenceAdapter は、実際に渡された実装クラスが何かを知らなくてよくなる。