[わかりやすさ重視]図解クリーンアーキテクチャ

2021年6月10日木曜日

Architecture

クリーンアーキテクチャの成り立ちを図を用いながら、順を追って紐解いてみます。

アイキャッチ

背景

クリーンアーキテクチャを調べていくと下記のような概念図や

クリーンアーキテクチャ概念図
出典: https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html

下記のような構成図に触れることになります。

クリーンアーキテクチャ構成図
出典: 「Clean Architecture 達人に学ぶソフトウェアの構造と設計」

言いたいことは何となく分かるのですが、初見でメリットが理解できませんでした。
クリーンアーキテクチャを解説する書籍「Clean Architecture 達人に学ぶソフトウェアの構造と設計」に記載があるようにアーキテクチャのルールの根幹はどれも同じであるようです。

そこで、既存のアーキテクチャのルールを参考にこのクリーンアーキテクチャを考えてみることにしました。

本記事ではレイヤードアーキテクチャの欠点を
SOLID原則に沿って補完していくことで、クリーンアーキテクチャをひも解いていきたいと思います。

はじめに

SOLID原則とは?

ソフトウェアの拡張性、保守性等を担保し、メンテナンスしにくいプログラムになることを防ぐための原則です。

S:SRP、単一責任の原則
O:OCP、解放閉鎖の原則
L:LSP、リスコフの置換原則
I:ISP、インタフェース分離の原則
D:DIP、依存性逆転の原則

SOLID原則の詳細はこちらの記事が参考になります。
イラストで理解するSOLID原則

クリーンアーキテクチャが必要な理由

それでは、さっそくなぜクリーンアーキテクチャが必要となるのか?
その動機をクリーンアーキテクチャのコンポーネントごとに見ていきます。

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を介してデータを取得するモジュールをUseCase Interactorとします。

そして分割するモジュールの依存関係は、先ほどの安定依存の原則に基づいて、
抽象度が高く安定しているEntitiesに、UseCase 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を実現しているモジュールが
Presenterである点に注意が必要となります。
ここでも安定依存の原則が基づいて依存関係が調整されます。

最後に少しだけ図の配置を整理してシステムの境界線を引いてみます。

上図を下図クリーンアーキテクチャを適用した場合の構成図と見比べてみます。

ほとんどの構成が同じかと思います。

クリーンアーキテクチャの図と異なる点は、
Applicationモジュールが存在する点。
DSと記載されたData Structureが存在しない点の2点です。

Applicationモジュールが存在する点に関しては、
UseCase InteractosとApplicationのどちらもアプリケーションルールの記述が責務ですので
一つのモジュールにまとめることも可能です。

次にデータ構造に関しては図が煩雑になるため割愛しているのみで、
明示的に書けば同じとなります。

このデータ構造が示す意味は、
依存する側は、依存されている側が定義しているデータ構造に合わせて
Interfaceを呼び出す必要があると強調していると理解しています。
仮に、ControllerがUseCase側のデータ構造でなく、
Controller側で定義したデータ構造をUseCase側で解釈しなければならないのであれば、
UseCaseは推移的にControllerに依存しているといえます。

この関係の場合、
変更箇所の多いモジュールのデータ構造に安定したモジュールが推移的に依存していることになり 前述の原則に反してしまいます。

さいごに

クリーンアーキテクチャをSOLID原則の観点で見ていきました。

いきなりクリーンアーキテクチャだけを見てしまうと難解に思えてしまいますが、
順を追ってみていくと既存の考え方の組み合わせのように見えます。

そして全ての問題を解決しているように書いていますが
クリーンアーキテクチャにも欠点はあると思っています。
クラス数の増大やデータ構造の載せ替えが、
メモリやパフォーマンスに影響する可能性は否めません。
ハードウェアスペックに制限がある環境においては一部バランス調整を余儀なくされると思います。

前向きに捉えれば製品環境と設計指針のバランスを調整し、変更容易性を高めていくことこそアーキテクトの醍醐味とも言えます。

AIで副業ならココから!

まずは無料会員登録

プロフィール

メーカーで研究開発を行う現役エンジニア
組み込み機器開発や機会学習モデル開発に従事しています

本ブログでは最新AI技術を中心にソースコード付きでご紹介します


Twitter

カテゴリ

このブログを検索

ブログ アーカイブ

TeDokology