たつのおとしごのしっぽ

技術に楽しくしがみつく えんじにあ の備忘録

Clean Architectureは設計に悩んだ時に見返したい

はじめに

良本の1つとされる「Clean Architecture」を読み切ったら、まとめたくなったので整理してみます。
最近上手く設計できず悩んでいる中でこの書籍を読んだので、良い設計とは何かの指針を知ることができたなと思っています。
ここで、まとめて設計で詰まった時に見返せるようにしたいと思います。
ざっくりとしかまとめていないので詳しくは書籍を読むことをお勧めします。

Clean Architecture 達人に学ぶソフトウェアの構造と設計 (アスキードワンゴ) Kindle版 Amazon

個人的に考える読み始めるタイミング

プログラミングをし始めて設計について考えだした、考えてみたいと思った方にお勧めしたい本です。
私のプログラミング歴はJavaC#です。あと、JavaScript、Kotlinをかじった程度です。
読んでて辛いなと思ったのは、作者のボブおじさんがハード寄りの知識が詳しく、ハードの知識やC++のコードが載っているので、そこが理解しにくかったです。ハードの知識などは調べたり職場の先輩に聞いたりして補いながら読み進めました。
あと、一度プログラミングの一機能任されて実装、設計したことがあると読んでて、ああ分かる分かると思う機会が多いと思います。

まとめ

引用を一応付けてますが、本書を読んでのまとめたものです。

良いアーキテクチャとは

Clean Architectureでは、最初の章に良いアーキテクチャとは何かを丁寧に説明してくれます。
実際、業務でプログラミングしていると優先度を下げてしまいがちな良いアーキテクチャとは何か?というところの認識を合わせています。

第2章 2つの価値のお話

変更の難易度は形状(技術の置き換えが容易かどうか)ではなくスコープに比例すべき。

ステークホルダーと開発者で変更の難易度に差が生まれるのは、変更がスコープに比例しないためだと思う。スコープに比例した変更になるようなアーキテクチャを目指したいですね。

ソフトウェアは、動作することよりも変更しやすいことに大きな価値がある。
端的に言えば変更できれば動作する可能性があるが、変更できないことは将来、変更を求められたときに対応できないから。

ボブおじさんは、ソフトウェアは変更できることに価値があるよと何回も言います。
開発現場で意識改革したいことの一つでもあると思います。

第4章 構造化プログラミング

プログラミングはテストによってが正しくないことを証明することによって正しさを明らかにする。
それゆえ、証明可能なプログラムでなければ正しくないことを証明できない。
証明可能なプログラムにするために分割して証明可能な小さな機能にする必要がある。

これは、統計学で正しさを証明するときと似ているなと思いました。
対立仮説「プログラムが正しくない」をテストによって有意水準5%の両側検定で対立仮説を棄却できないよね、みたいな感じですね。

第3章 パラダイムの概要

構造化プログラミングは、直接的な制御の移行に規律を課すもの。
オブジェクト指向プログラミングは、間接的な制御の移行に規律を課すもの。
関数型プログラミングは、代入に規律を課すもの。
つまり、これらはgoto文、関数ポインタ、代入を奪っている。

第6章 関数型プログラミング

関数型プログラミングの変数は変化しない。
競合やデッドロック状態や並行更新など変更されないため問題が発生しないのが魅力。
ただ不変性はリソースの問題は残る。
その場合は可変不変コンポーネントに分けて可変コンポーネントトランザクションメモリで変数を管理して対応する。

関数型プログラミングは最近になって見るようになってきましたが、上記のメリットがあるようですね。
以下が参考になります。
関数型プログラミングはまず考え方から理解しよう - Qiita

良いアーキテクチャにするための設計原則とは

クリーンなコードを書くための設計原則などを説明してくれます。
原則はいくつも出てきますが、クリーンアーキテクチャに関わらず有名な原則のためこれを機におさえておきたいと思います。

第5章 オブジェクト指向プログラミング

ソフトウェアはソースコードの依存関係と制御の流れは同じである。
なぜなら、システムの振る舞いにより制御の流れが決まり、制御の流れによりソースコードの依存関係が決まるから。
しかし、ポリモーフィズムを使うと依存関係が制御の流れと逆転することができる。
そのため、制御の方向によらず依存関係が制御できる。

依存関係が制御できるとコンポーネントを分けたりデプロイを独立させることができる。
これがプラグインアーキテクチャになる。

オブジェクト指向の章で出てくるポリモーフィズムにより依存関係を逆転させることが出来ることは、「Clean Architecture」で重要な考え方の1つです。
依存関係を逆転させることで、依存関係をプラグインしやすいように変えることということですね。

SOLID原則

SOLID原則とは、関数やデータ構造をクラスへどう組みこむかやクラスの相互接続についてどのようにするのがベストかを記述したもので、以下の性質を持つモジュール単位のソフトウェア構造を作ることが目的となる。
- 変更に強い
- 理解しやすい
- コンポーネント基盤として多くのシステムで利用できる
第7章 SRP:単一責任の原則より

SOLID原則について簡単に書く - Qiitaが分かりやすい。

SRP:単一責任の原則

モジュールを変更する理由はただ1つだけであるべき。
これは、モジュールは1つのアクターに対して責務を負うべきであり、アクターの異なるコードは分割すべきということ。
解決策の1つに関数を他のクラスに移動することでデータから関数を切り離すことがあげられる。
その時、Facadeパターンを使えばそれぞれのクラスの追跡はしなくてよくなる。
第7章 SRP:単一責任の原則

クラスの追跡はしなくて良いという点は、Facadeパターンを使えばクラスを分けても多数のクラスを呼び出す先は1つにできるため、呼び出す側はクラスを管理したりしなくてよくなるということですね。
Facadeパターンについては、以下が参考になります。 JavaでFacadeパターン - Qiita

OCP:オープン・クローズドの原則

ソフトウェアの構成要素は拡張には開いていて、修正には閉じているべき。
そのためには、
- システムをコンポーネントに分割して依存関係を階層構造にする
- コンポーネントの関係が単一方向にする(変更の影響を及ぼしたくない方に矢印が向く)
を意識する必要がある。

依存の上位にくるのはビジネスルールを含んでいるInteractorであり、下位が変わっても上位は変わらないようにすること。
そのためにIFに分離しコンポーネントの依存関係を制御する。
第8章 OCP:オープン・クローズドの原則

Interactorは、ユースケースを実現するためのコンポーネントです。
ビジネスルールを含むようなコンポーネントは、依存関係の上位にすることで下位のデータ入出力などの影響を受けないようにします。
第5章での話はOCP原則の実現につながりますね。

LSP:リスコフの置換原則

IFの派生型がIFの型できちんと置換できるべき。
IFの派生型を気にせず他のクラスがIFの型で派生型を使えること。
IFで定義されているフィールドやメソッドが適切に継承されていないと、継承されていない特別なフィールドやメソッドに対応するための複雑な仕組みを追加することになってしまう。
第9章 LSP:リスコフの置換原則

これは、実装のルールのように見えてアーキテクチャレベルで考慮していかないといけないということです。
確かにクラス図とかフィールドやメソッドも考慮しますね。

ISPインターフェイス分離の原則

必要としないモジュールに依存すべきでない。
上位のアーキテクチャレベルでも不要なモジュールに依存していると、使っていない機能にモジュールが依存することになったり不要なものまで再デプロイが必要になってしまう。
第10章 ISPインターフェイス分離の原則

これには、IFに分離することよって依存関係を逆転しIFを使っているコンポーネントに変更があってもIFの実装クラスは再デプロイの必要がなくなるようにする必要がありそうですね。
このあたりでIFによる依存関係の逆転はボブおじさんが好きな考えなのかなとおもいはじめます。

DIP:依存関係逆転の原則

ソースコードの依存関係が(具象ではなく)抽象だけを参照しているべき。
DIPでは、OSやプラットフォームは変化しないとみなすことが多い。

コーディングレベルのプラクティスとして、以下があげられる。
- 変化しやすい具象クラスを参照しない
実装ではAbstract Fatcory パターンを使って実現する。
そうすると、依存性は具象から抽象に、処理は逆になる。
- 変化しやすい具象クラスを継承しない
- 具象関数をオーバーライドしない
実装では元の関数を抽象関数にしてそれに対する複数の実装を用意する。
- 変化しやすい具象を名指しで参照しない
DIPの言い換え。
ただ、mainコンポーネントは具象クラスを生成するためDIPには違反してしまう。
あくまでDIPに満たない具象コンポーネントを最小限にするということである。
第11章 DIP:依存関係逆転の原則

ここで、mainコンポーネントについて話が出たため、第26章のまとめも合わせておく。

mainコンポーネントは、究極的な詳細(最下位レベルの方針)である。
システムの最初のエントリーポイントになり、オペレーティングシステム以外に、このコンポーネントに依存しているものはない。

FactoryやStrategyなどのグローバルな要素を作成し、システムの上位の抽象部分に制御を渡すことが、このコンポーネントの仕事になる。
ここでのポイントは、mainコンポーネントはクリーンアーキテクチャの円の最も外側にある下位レベルのモジュールであることだ。
それは、上位レベルのシステムのためにすべてを読み込み、制御を渡すものである。
そのためmainコンポーネントをアプリケーションのプラグインと考えよう。
初期状態や構成を設定して、外部リソースを集め、アプリケーションの上位レベルの方針に制御を渡すプラグインである。
プラグインなので、アプリケーションの設定ごとに複数のMainコンポーネントを持つこともできる。
第26章 メインコンポーネント

mainコンポーネントでFactoryのインスタンスを作成する必要があるので、DIPを満たさない具象コンポーネントもあるということですね。
DIコンテナを使う時もmainメソッドでクラスのオブジェクト生成したりしますね。

コンポーネントの凝縮性

どのクラスにどのコンポーネントを含めればいいかの判断に関して以下の原則が参考になる。

REP:再利用・リリース等価の原則

再利用の単位とリソースの単位は等価になる。 コンポーネントには一貫するテーマや目的があるモジュールを集めたものである。 さらにコンポーネントを構成するクラスやモジュールはまとめてリリース可能であるべき。 そうでないとコンポーネントを再利用できなくなる。 第13章 コンポーネントの凝縮性

CCP:閉鎖性共通の原則

同じ理由、同じタイミングで変更されるクラスをコンポーネントにまとめること。
変更の理由やタイミングが異なるクラスは別のコンポーネントに分けること。

SPRをコンポーネント用に言い換えたもの。
変更箇所が1つのコンポーネントに閉じていればソフトウェアのリリースやデプロイ作業を最小限に抑えられる。
第13章 コンポーネントの凝縮性

CRP:全再利用の原則

コンポーネントのユーザーに対して、実際には使わないものへの依存を強要してはいけない。
密結合していないクラスを同じコンポーネントにまとめるべきではない。
不要なものには依存しない。
ISPを一般化したもの。
現在の懸念事項に合わせてREP、CCP、CRPをバランスを取るのがアーキテクトの腕の見せ所。

さらに懸念事項は時間の経過や成熟度によって変わることも考慮しなければならない。
開発の初期段階なら再利用性REFより開発のしやすさCCPの方が大事になる。
第13章 コンポーネントの凝縮性

コンポーネントの結合

コンポーネントの関連を扱う原則について挙げる。

ADP:非循環依存関係の原則

コンポーネントの依存グラフに循環依存があってはいけない。
リソース可能なコンポーネントに分割すれば、作業の単位がコンポーネント単位になり、コンポーネントごとに担当を割り当てられる。
コンポーネント単位で分割されていればコンポーネントの使用可否をチームごとに選択したりでき、他の開発者の影響を受けなくなる。

しかし、これを機能させるには循環依存が起こらないよう、コンポーネントの依存構造を管理しておく必要がある。
循環依存の解消には、
1.DIPを適用して、依存関係を逆転させる。
2.循環依存の原因になっている依存先と依存元のコンポーネントの両方が依存する新しいコンポーネントを作り、依存しているクラスをそのコンポーネントに移す。
第14章 コンポーネントの結合

SDP:安定依存の原則

安定度の高い方向に依存すること
SDPを満たしていれば、手軽に変更できるように作ったモジュールが変更しづらいモジュールから依存されていない 。
モジュールの変更しづらさは多くのコンポーネントから依存されているかどうかである(これを本書では安定度と呼んでいる)。
安定度の指標は依存しているコンポーネントの数が目安になる。

また、図を書くときには安定度の低いコンポーネントを上に描くことで、下から上の矢印に依存関係が向いている箇所はSDPに反しているとすぐわかるようになる。
ただし、全てのコンポーネントが安定度が高い必要はなく、ビジネスルールなど簡単に変更したくないコンポーネントを対象に必要に応じて安定度を高めればよい。

本書では、安定度の指標の計算なども載っている。あくまで客観的に安定度を測ることが重要だと感じる。
また、ビジネスルールが含まれているから安定度を必ず高くしなければならないというわけでもなく、あくまでプロジェクトによって「簡単に変更したくないコンポーネント」を見極める必要があると感じる。

プロジェクトごとに上位のコンポーネントは異なるという点については、世界一わかりやすいClean Architecture - nuits.jp blogを見ると納得できます。
Clean Architectureを読んでると、理想論過ぎて実際のプロジェクトにどう反映したらいいんだ…となってしまいますが、上記のサイトを見ると、シンプルにまとまっているためClean Architectureの導入するためにどう読みかえればいいかが分かります。

SAP:安定度・抽象度等価の原則

コンポーネントの抽象度は、その安定度と同程度でなければいけない。
ビジネスやアーキテクチャに関する決定は頻繁に変わるべきではないため安定度の高いコンポーネントになる。
ただし、それでは柔軟性に欠けるため、OCPを満たす抽象クラスを使って既存コードを変更せずに拡張させることで対応する。
(抽象度に関する具体的な指標については本書にて)

コンポーネントの安定度を高くするにはインターフェイスや抽象クラスで構成する。
SDPとSAPをまとめると、抽象度の高いコンポーネントに依存するようにすることが必要ということになる。

この原則たちでも開発の利便性と論理的な設計のトレードオフが生じるため、テーマに合った方針を選定すべきであるというのがボブおじさんの考え方です。

アーキテクチャとは?

ソフトウェアの開発・デプロイ・運用・保守を容易にするには、出来るだけ長い期間・出来るだけ多くの選択肢を残すようなアーキテクチャにすること。
アーキテクチャの主な目的は、システムのライフサイクルをサポートすること。
最終的な目的は、システムのライフタイムコストを最小限に抑え、プログラマの生産性を最大にすること。

アーキテクチャは、詳細(方針を実現するのに必要なもの)を気にせず方針(ビジネスルール)を構築することで、詳細の決定をなるべく遅らせる。
よって、方針と詳細を切り離すことが重要である。
なぜなら、方針が詳細まで知っていると、方針が変わった時に詳細に合わせて変換する対応を入れなければならなくなり開発コストが莫大になってしまう可能性があるから。
第15章 アーキテクチャとは?

独立性

デプロイ

即時デプロイを目指すためにコンポーネントを適切に分離・分割する必要がある。
重複は偶然アルゴリズムが似ているなど偶然の重複の場合もあるので本物の重複か見極めて統一する。
レイヤーやユースケースの切り離しはソース、デプロイ、サービスレベルで切り離しが可能。
最適なレベルはプロジェクトが進むにつれ最適なものは変わってくる。
最初はソースレベルでもよかったがデプロイレベルでの切り離しが必要になる、など。
別物を統一させると後で切り離すのが大変なので出来るだけ切り離しておくことがおすすめ。
第16章 独立性

16章は、今までのまとめのような感じだが、個人的に覚えておきたいデプロイに関する内容について、まとめておく。
重複はまとめがちだが、アーキテクチャレベルで本当に同じものであるか考える必要があると感じた。

境界線

アーキテクチャでは境界線を引いてソフトウェアの要素を分離しお互いが分からないように制限することが必要である。
何故なら、システムのユースケースと関係のない詳細の決定を遅らせるため。

境界線は重要なものと重要でない者の間に引く。
例えば、DBはビジネスルールが間接的に使えるツールであるため境界線がある。
あくまでビジネスルールはデータが取得保管できるということのみ知っていればよい。

境界線は変更の軸がある所に引く。
境界線をはさんだコンポーネントは変更の頻度や理由が異なる。(これはSRPに該当する)
境界線を引くときビジネスルールに関係ないコンポーネントプラグインにし、DIPによりビジネスルールに依存するように意識する。
第17章 バウンダリー:境界線を引く

本書でまとまっているが、実際はサブシステムやコンポーネントレベルだと境界線を決める経験が大事だと思う。

ビジネスルール

エンティティ

ビジネスデータ(最重要ビジネスルールで使うデータ)を操作する最重要ビジネスルール(マネーを生み出す・節約するルールや手続きのこと)を含んだもの。

ユースケース

エンティティに含む最重要のビジネスルールとは異なり、アプリケーション固有のビジネスルール。
ユーザーとエンティティのインタラクションを支配するアプリケーションの固有ルール。
エンティティはDIPにより自身を制御するユースケースは知らない。
エンティティとDBのデータ構造をまとめてはいけない。CCP,SRPに反する。
第20章 ビジネスルール

エンティティとユースケースの区別は、一緒になってしまったりするため違いはきちんと把握しておくべきだと感じた。
ここがアーキテクチャ肝かと思うが、本書では意外にもあっさりしている。

クリーンアーキテクチャ

クリーンアーキテクチャは、今までのアーキテクチャでもつ特性(フレームワーク非依存、テスト可能、UI非依存、DB非依存、外部エージェント非依存)を単一の実現可能ないアイデアに統合したものとしている。

クリーンアーキテクチャは、依存性のルールとして、以下をあげている。
ソフトウェアの依存性は内側だけに向かっている必要がある。

また、それぞれのコンポーネントは以下の位置付けになる。
エンティティ:最重要ビジネスルールをカプセル化したもの。
ユースケース:アプリケーション固有のビジネスルールが含まれているもの。
インターフェイスアダプター:ユースケースやエンティティに便利なフォーマットからDBなど外部エージェントに便利なフォーマットに変換するアダプター。逆変換も含まれる。
フレームワークとドライバー:フレームワークやツールなど。DBもここに該当する。

クリーンアーキテクチャの図を見ると円は4つに見えるが、円の数は固定ではない。
ただし、依存性の方向だけは常に適用される。
制御の流れが外側から内側に向かう場合はIFを使って依存関係を逆転させる。
境界線を超えてデータを渡す場合は単純なデータ構造など内側にとって便利な形式であることが必要である。
なぜなら、DBに依存したデータなどを渡すと依存性のルールに反するため。
第22章 クリーンアーキテクチャ

本書でもっとも言いたいことが書かれている章。
文章や図だけでは把握しにくい場合は、ここでコードを見ると理解が深まる。
C#が読めるなら実装クリーンアーキテクチャ - Qiitaが参考になります。

プレゼンターとHumble Objectパターン

クリーンアーキテクチャでは、プレゼンターはHumble Objectパターンの一種としている。
Humble Objectパターンとは、テストしにくい振る舞いとテストしやすい振る舞いをモジュール、クラスとして分けるデザインパターン
例えばGUIはテストしにくく、GUIの振る舞いはテストしやすいためViewとPresenterに分かれる。
ViewModelはViewに配置する文字列・真偽値・列挙型などをPresenterからうけとるだけ。
アーキテクチャの境界線にHumble Objectパターンが潜むようにするとテスト容易性が向上する。
第23章 プレゼンターとHumble Object

Humble Objectパターンは、テストしにくいものを分けることで、どこまではテストして保証するか明確にすることができる。
以下が参考になる。
Humble Object - 八葉の日記

非同期プログラミング - 非同期コードの単体テスト: テストを容易にする 3 つの解決策 | Microsoft Docs

部分的な境界、レイヤと境界線

きちんとしたアーキテクチャの境界を作るとコストが高いが、できるだけ境界をのこしておきたいというジレンマを実現するために部分的な境界を検討することになる。
部分的な境界を構築するには独立したコンポーネントを1つのコンポーネントにまとめることである。
独立してコンパイルやデプロイが可能なコンポーネントを準備したあとで、それらを同じコンポーネントにまとめるというものである。
相互インターフェイスも、入出力のデータ構造も、すべて設定しておくが、コンパイルやデプロイはひとつのコンポーネントで済むようにしておくのである。
あとからコンポーネントを再度分離するのはめんどくさいからである。
アーキテクチャの完全な境界は双方向の分離を維持するため、両側にIFを使用するというもの。
だが、双方向の分離を維持するというのは、初期のセットアップも継続的な保守もコストが高い。

そのため、部分的な境界の実現として、以下のパターンをあげている。
Strategyパターン
片側だけ境界を持つようにする。
ただし、片側しかIFがないため上位のコンポーネントが下位のコンポーネントに依存するような実装ができてしまう。
綺麗なアーキテクチャは、規律で縛るしかない。

Facadeパターン
Strategyパターンよりシンプルに境界を作ることができる。
ただし、Facadeを呼び出すクラスはFacadeで管理している全クラスに推移的に依存していることに注意してほしい。
静的言語では、Facadeで管理しているクラスのいずれかのソースコードを変更すると、Facadeを呼び出すクラスも再コンパイルが必要になる。
また、この構造でも簡単に上位のコンポーネントが下位のコンポーネントに依存するような実装ができる。
きちんとした境界線を作るのはコストがかかる。
境界線の作り方はいくつかあるがメリット・デメリットが存在する。境界線をいつ、どのような形(完全・部分的)で作るか決める必要がある。
第24章 部分的な境界

しかもこれは、1回限りの決定ではない。プロジェクトの開始時に、実装する境界と無視する境界を決めればいいわけではない。
常に見張る必要がある。
境界が必要になりそうなところに注目して、境界がないために発生している 摩擦の兆候を感じ取ってほしい。
そこから、境界を実装するコストと無視するコストを比較検討し、その決定を何度も評価する。
無視するコストよりも実装するコストが低くなる変曲点で、境界を実装することがゴールである。
第25章 レイヤと境界

さいごに

設計がきれいでも実装方法が複雑だと設計が崩れてしまう。
チームの規模、メンバーのスキル、ソリューションの複雑さ、時間と予算について考慮する。
第34章 書きこのしたこと

特に時間・予算とチームメンバーのスキルに左右されるように思う。
新しいことを始めるなら時間・予算の余裕のあるプロジェクトやプロジェクトの初期段階だろう。
チームメンバーでClean Architectureを知らない人がいるとやはりコストはかかるだろう。
適切なアーキテクチャの選択は奥深く難しいと分かる。

本書では、実装方法が複雑にならないようアーキテクチャの原則はコンパイラに任せるべきとしている。
ボブおじさんは、ほかのアプローチとしていくつかのパッケージング方法を紹介している。

水平方向のレイヤードアーキテクチャ
ビジネスロジック、永続化、コード用の3層に分ける。
複雑になりすぎないように早く作るにはよい。
規模が大きいともう少し分ける必要がある。
垂直方向の機能ごとのアーキテクチャ
ユースケースが変更になった時に変更するパッケージが1つで済む。
コンポーネントによるアーキテクチャ
ビジネスロジックと永続化を1つにまとめてコンポーネントにする。
レイヤーを超えた依存を防ぐ。
アーキテクチャの原則をチェックするのはコンパイラに任せるべき。