クリーンアーキテクチャに関して、いきなり完成形を見てもなぜそのような構成なのかがいまいち腹落ちしませんでした。
そのためクリーンアーキテクチャの成り立ちを自分なりに考えてみました。
はじめに
言いたいことは何となく分かるのですが、初見ではいまいち何が良いのか分かりませんでした。
クリーンアーキテクチャを解説する書籍Clean Architecture 達人に学ぶソフトウェアの構造と設計に記載があるようにアーキテクチャのルールはどれも同じであるようです。
そこで、既存のアーキテクチャのルールを参考にこのクリーンアーキテクチャを考えてみることにしました。
本記事では、レイヤードアーキテクチャの欠点をSOLID原則に沿って補完していくことで、クリーンアーキテクチャをひも解いていきたいと思います。
前置き
SOLID原則とは?
ソフトウェアの拡張性、保守性等を担保し、メンテナンスしにくいプログラムになることを防ぐための原則です。
S:SRP、単一責任の原則
O:OCP、解放閉鎖の原則
L:LSP、リスコフの置換原則
I:ISP、インタフェース分離の原則
D:DIP、依存性逆転の原則
詳細は、下記リンクの記事が参考になります。
https://postd.cc/solid-principles-every-developer-should-know/
Clean Architectureとは?
クリーンアーキテクチャの説明は発端の記事やこの和訳等をご参照ください。
本題
Frameworks & Drivers(Data Access)
クリーンアーキテクチャのInterface Adaptersがなぜ必要か?
をレイヤードアーキテクチャの欠点から見ていきます。
依存性逆転の原則 (DIP: Dependency Inversion Principle) その①
あるソフトウェアがレイヤードアーキテクチャで構成されていたとします。
レイヤードアーキテクチャは処理の流れに沿ってモジュール間が依存しています。
見通しが良く、直感的に非常に分かりやすいアーキテクチャです。
一方、デメリットもあります。それは、Domain層が、Infrastructure層に依存している点です。この依存方向により、Infrastructure層を変更すると、Domain層が影響を受けます。
注意したいのは、
依存していることが問題ではなく、
依存している方向が問題である。ということです。
システムを構築する上でモジュール間の依存は避けては通れません。依存関係が発生することのそれ自体は何ら悪ではありません。問題になるのは、依存関係の方向なのです。
レイヤードアーキテクチャに目を向けると、Domain層はビジネス的価値を記述し、Infrastracture層は技術的詳細を記載します。一般的に詳細を記述しているInfrastracture層の方が変更可能性が高いです。
安定しているDomain層が、安定していないInfrastracture層に依存するという
依存関係の方向が下記の原則に違反しているのです。
安定依存の原則(SDP:The Stable Dependencies Principle)
これはモジュール間の依存関係は安定している方向に向いてなければならないという考え方です。
この考え方に基づくと
比較的抽象度の高い安定したDomainが、
技術的な詳細である安定していないInfrastructureに依存している状態
はこの原則に反しています。
そこでこの依存関係を解決する考え方として依存性逆転の原則があります。
依存性逆転の原則に従うとDomainとInfrastructureの関係は下記のようになります。
Domainは抽象であるInterfaceに依存し、
技術的な詳細であるInfrastructureもまた抽象であるInterfaceに依存することで
依存関係の方向を逆転させます。
Interfaceの名前は、Domainロジックに必要なデータにアクセスするためのInterfaceであるためData Access Interfaceとします。
Interface Adapters(Repository)
依存性逆転の原則 (DIP: Dependency Inversion Principle) その②
Interfaceを設けたことで抽象に依存できるようになりました。
しかし、一つ問題があります。
実装を考えると結局Domainが、Data Access Interfaceを呼び出すときにInfrastructureをインスタンス化する必要があります。
DataAccessInterface interface = new Infrastructure();
interface.getData();
そこでDomainからInfrastructureのインスタンスを隠蔽するため、Infrastructureのインスタンス化を行うData Accessを配置することでこの問題を解決します。
DataAccessInterface interface = new DataAccess();
interface.getData();
Data AccessがInfrastructureを抽象化します。
@Override
public void int getData(){
Infrastructure infra = new Infrastructure();
infra.getDataFromInfra();
}
このような構成にすることにより、Domainに対して、データの実装を抽象化します。
そして、このようなデータアクセス手段、データの永続化を抽象化するオブジェクトをRepositoryと呼びます。
デザインパターンとしては、リポジトリパターンとして知られています。
Frameworks & Drivers(Controller)
解放閉鎖の原則 (OCP:Open/Closed Principle)
次に視点を変えてUI(Presentation)とApplicationの依存関係に目を向けてみます。
言うまでもないですが、UIは変更が非常に多いです。多くの変更を強いられます。
この時UI(Presentation)は、Applicationに依存しているため、UI(Presentation)
のソースコードを変更した場合、Applicationのソースコードも含めてコンパイルする必要があります。(静的型付け言語の場合)。
UI(Presentation)の修正に伴ってApplicationを含めたテストも必要となるかもしれません。
また、UI(Presentation)が、Applicationの実装を知りすぎてしまうことも問題となる可能性があります。
優先すべきはUI(Presentation)の修正をApplicationに影響させないことですが、Applicationの変更からUI(Presentation)を保護しておきたいのも事実です。
こうした事態を解決する考え方として解放閉鎖の原則の考え方があります。
この考え方に基づいて変更の多いUI(Presentation)からApplicationを閉じるため、Application Interfaceを設けます。
また、"依存性逆転の原則 (DIP: Dependency Inversion Principle) その②"におけるData Accessと同じようにUI(Presentation)にインスタンスを隠蔽するためControllerを配置します。(責務的な理由もありますが後述)
このような構成にすることにより、
UI(Presentation)と、Application間の依存関係の方向を制御し、モジュール間の影響を小さくします。
Enterprise Business Rules & Application Business Rules(Usecase & Entity)
単一責任の原則 (SRP:Single Responsibility Principle) その①
今度は依存関係ではなく、各モジュールの責務に目を向けてみます。
Infrastructureは、最も技術的な詳細を知っており、その詳細な制御によってデータを提供します。
データベースであれば、データベースの制御方法を知っているのがこのモジュールです。
Data Accessは、DomainからInfrastructureを抽象化するため、InfrastructureとDomainの仲介を行います。
Domainは、Applicationから要求を受けた後、要求に応じてデータを取得してから、ビジネスロジックを実行します。
この時、Domainが変更される契機について考えてみます。
一つは、ビジネスルールに変更があった場合です。そして二つ目に、取得するデータに変更があった場合です。
このようにDomainは、2つの変更理由によって修正が加えられます。この問題として、一方の修正によってもう一方のコードに予期せぬ影響を及ぼすかもしれません。
こうした問題への考え方として単一責任の原則があります。
この考え方によると一つのモジュールは二つ以上の変更理由で変更されてはなりません。
言い換えれば、二つ以上の変更理由を持つモジュールは一つになるよう分割すべきなのです。
この考えに基づいて、Domainを分割します。
ビジネスルールを提供するモジュールをEntitiesと、Data Access Interfaceを介してデータを取得するモジュールをUse Case Interactorとします。
そして分割するモジュールの依存関係は、先ほどの安定依存の原則に基づいて、抽象度が高く安定しているEntitiesに、Use Case Interactorを依存させます。
Interface Adapters(Presenter)
単一責任の原則 (SRP:Single Responsibility Principle) その②
先ほどからUI(Presentation)と記載しているモジュールがあります。
Viewを表示する責務を担うモジュールと、Controllerから受け取ったデータを解釈しViewに引き渡すモジュールが混在しているため括弧書きになっていると言えます。
"単一責任の原則 (SRP:Single Responsibility Principle) その①"と同じ考え方に基づき分割します。
このPresenterは、Controllerから受け取ったApplicationからのデータを解釈し、Viewに引き渡すことが責務となります。
インタフェース分離の原則 (ISP:Interface Segregation Principle)
次にControllerに目を向けます。"解放閉鎖の原則 (OCP:Open/Closed Principle)"で、Controllerの責務は後述と書きましたが、
ここで、Controlelrの責務に関して考えます。
Controllerの責務はViewの状態を受け取ってApplication Interfaceを介して、Applicationが要求するデータ形式でデータを引き渡すことです。
しかし、先ほどの変更によってViewにデータを引き渡すPresenterは、Controllerを介してしかApplicationのデータを受け取ることができません。
この依存関係において一つの問題が生じます。
Controllerは本来責務でない実装をしなければなりません。Application InterfaceのリターンをPresentationに渡す実装が必要となります。
また、View起点でなくInfrastructure起点のデータをViewに表示する場合においては、
Controllerは単にPresentationにデータを引き渡すためだけにApplication Interfaceを呼び出さなければなりません。
こうした問題を解決する考え方にインターフェース分離の原則があります。
この考え方に基づき、Controllerが使用しないInterfaceを呼び出す実装が必要ないようにInterfaceを分割します。
Controllerは自身の責務に沿ったInterfaceのみを呼び出せるようにInput Boundaryを配置し、
Presenterも自身の責務に沿ったInterfaceのみを実装できるように、Output Boundaryを配置します。
ここでOutput Boundaryを実現しているモジュールがPresentorである点に注意が必要となります。ここでも安定依存の原則が基づいて依存関係が調整されます。
最後に少しだけ図の配置を変えてシステムの境界線を引いてみます。
上図を下図クリーンアーキテクチャを適用した場合の構成図と見比べてみます。
ほとんどの構成が同じかと思います。
クリーンアーキテクチャの図と異なる点は、Applicationモジュールが存在する点。DSと記載されたData Structureが存在しない点の2点です。
Applicationモジュールが存在する点に関しては、
UseCase InteractosとApplicationのどちらもアプリケーションルールの記述が責務ですので一つのモジュールにまとめることも可能です。
次にデータ構造に関しては図が煩雑になるため割愛しているのみで、明示的に書けば同じとなります。
このデータ構造が示す意味は、依存する側は、依存されている側が定義しているデータ構造に合わせてInterfaceを呼び出す必要があると強調していると理解しています。
仮に、ControllerがUse Case側のデータ構造でなく、Controller側で定義したデータ構造をUse Case側で解釈しなければならないのであれば、
Use Caseは推移的にControllerに依存しているといえます。この関係の場合、変更箇所の多いモジュールのデータ構造に安定したモジュールが推移的に依存していることになり前述の原則に反してしまいます。
さいごに
クリーンアーキテクチャをSOLID原則の観点で見ていきました。
いきなりクリーンアーキテクチャだけを見てしまうと難解に思えてしまいますが、順を追ってみていくと既存の考え方の組み合わせのように見えます。
そして全ての問題を解決しているように書いていますがクリーンアーキテクチャにも欠点はあると思っています。
クラス数の増大やデータ構造の載せ替えが、メモリやパフォーマンスに影響する可能性は否めません。ハードウェアスペックに制限がある環境においては一部バランス調整を余儀なくされると思います。
自身の製品環境と設計指針のバランス調整こそアーキテクトの醍醐味とも言えますが。