CleanArchitectureを読んで

Posted on 2020/12/06

TOC

はじめに

CleanArchitectureを読んで記憶に残しておきたいポイントをメモに残しました。 各章が真に伝えたいことをメモしているわけではないのでご留意ください。

ポイント

第2章

ソフトウェアの価値は「振る舞い」と「構造」。ステークホルダーは「振る舞い」を重視するがプログラマは「構造」を重視する。
重要度×緊急度のマトリクスで考えると「振る舞い」は緊急だが重要なものと重要でないものがある。一方の「構造」は重要だが緊急なものと緊急でないものがある。そのためジレンマが生じる。 プログラマは保守容易性に関わる構造を保持する責任を伴うことを意識すべきである。

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

「テストはバグが存在しないことではなく、バグが存在することを示すものである」という言葉がある。 プログラムが正しい証明はできないが、十分量のテストをすることで正しくないことを証明できず、目的のために十分に正しい(真である)とみなすことができる。

現代の言語はgoto文をサポートしていない。好き勝手に制御できず、テスト可能な単位(モジュール、コンポーネント、サービス)単位で機能分割される。

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

ポリモーフィズムは協力なパワーを持っており、依存関係を逆転することでビジネスルールをUIやDBから独立させることができる。 OOは何かというと、カプセル化、継承、ポリモーフィズム等が挙げられるが、「ポリモーフィズムにより依存関係を制御する能力」が最も重要な能力である。

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

競合状態、デッドロック状態、平行更新の問題の原因は可変変数にある。 関数型プログラミングでは変数は変化しない。 変数を全く変化させないのは現実的ではない。一般的な妥協案としては「可変コンポーネント」「不変コンポーネント」の2つに分離すること。

イベントソーシングは状態ではなく取引を保存するという戦略。処理能力の上昇が背景。 削除・更新がされないため、CRUDではなくCRのみになり、平行更新の問題も発生しない。これにより完全な関数型を実現できる。

第2部まとめ

以下3つのプログラミングパラダイムは規律を課すことで(何かをさせないことで)品質の高い設計を実現する。

  • 構造化プログラミングは、直接的な制御の移行に規律を課す
  • OOPは、間接的な制御の移行に規律を課す
  • 関数型プログラミングは、代入に規律を課す

第7章 SRP(単一責任の原則)

名称がわかりにくいが、単一のアクター(ステークホルダー・ユーザー等)に責務を負うべきという原則。 あるクラスが複数アクターに依存していると、1つのアクターに起因する変更要件によって別のアクターにも影響が出てしまうことが理由。 詳細の動きをFacadeで隠す(処理を移譲する)ことでクラスを分割できる。

この本とは別で、SRPは「クラスを変更する理由はたったひとつだけであるべき」と言われるが、書籍上は上記のように踏み込んでいる。 ただ、Facadeパターンを使うのは「変更理由は1つであるべき」に対する解決策のように見える。「単一アクターに責務を負うべき」に対してはFacadeではなくVisitorパターンを使うのが適切?(処理内容だけ移譲するため)

第8章 OCP(オープン・クローズドの原則)

「ソフトウェアの構成要素は拡張に対しては開いていて、修正に対しては閉じていなければならない」という原則。 既存の成果物を変更せず(修正なしに)機能拡張できるべきである。

これはソフトウェアアーキテクチャを学ぶ根本的な理由。

コンポーネントレベルでは少し意味が変わり、ControllerコンポーネントからInteractorコンポーネント(DDDで言うService層とDomain層など)に依存することで、Controllerコンポーネントの振る舞いを変更した時にInteractorコンポーネントに影響が出ないようにする。 コンポーネント単位で依存関係を制御することで修正に対して閉じることが実現できる。

第9章 LSP(リスコフの置換原則)

「S型のオブジェクトo1の各々に、対応するT型のオブジェクトo2が1つ存在し、Tを使って定義されたプログラムPに対してo2の変わりにo1を使ってもPの振る舞いが変わらない場合、SはTの派生型であると言える」という原則。 JavaではInterfaceに対して複数の実装クラスが存在する場合、使い方(使用するメソッド)を同じにしておけば自然に置換可能になりそう。

アーキテクチャレベルでも、RESTfullサービスのURLを統一することでリスコフの置換原則に違反しないようにすべきである。

第10章 ISP(インターフェイス分離の原則)

依存関係を少なくすることが目的。 再コンパイルと再デプロイの範囲を狭めることができる。

アーキテクチャレベルでは、依存先が減ることで障害範囲を狭くすることにも繋がる。

第11章 DIP(依存性関係逆転の原則)

ソースコードの依存関係が具体ではなく抽象だけを参照するようにする。 具体より抽象インターフェイスの方が変更しにくく、良いアーキテクチャはインターフェイスの変更なしに新しい機能を実装できる。 処理の流れとソースコードの依存性が逆になるため、「依存性関係逆転」という名前が付けられた。

第12章 コンポーネント

コンポーネントはデプロイの単位を指す。Javaではjarを指す。

第13章 コンポーネントの凝集性

コンポーネントレベルにおける原則は以下3つである。

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

コンポーネントにはテーマ・目的がなければならない。 それらはまとめてリリース可能でなければならない。

閉鎖性共通の原則(CCP)

変更の理由が異なるクラスは別のコンポーネントに分けるべきである。 同じタイミングで変更されるクラスは物理的・概念的に密接に関連しているため 同じコンポーネントに含めるべきである。 これはOOPのクローズドと同じ意味であり、修正範囲を狭めることでリリース範囲・再デプロイ範囲を狭めることに繋がる。

SRP(単一責任の原則)も合わせて考えると、”同じ理由で変更するものをひとまとめにする”ということが重要。

全再利用の原則(CRP)

コンポーネント単位で不要なものに依存しないようにすべきである。 インターフェイス分離の原則(ISP)では不要なメソッドを持つクラスに依存しないようにすべきと述べたが、 全再利用の原則では不要なクラスを持つコンポーネントに依存しないべきと述べている。

第14章 コンポーネントの原則

非循環依存関係の原則(ADP)

コンポーネントの依存グラフに循環依存があってはならない。 各コンポーネントでバージョンを管理し、依存先の現時点で使っているバージョンで動作確認をしてリリースする。コンポーネントを使用する側は状況に応じて依存先コンポーネントの最新版を適用する。 循環依存を解決するには依存性関係逆転の原則(DIP)を使用する。場合によっては新たなコンポーネントを使用し、互いのコンポーネントが新しいコンポーネントを向くようにする。

コンポーネントkの依存構造はクラス設計前に決めるのではなくシステムの論理設計に合わせて育てていく。

安定依存の原則(SDP)

コンポーネントの依存関係は、安定度の高い方向に依存するべきである。 安定している(stable)とは簡単には動かせないこと(=not easily moved)を意味する。 多くのコンポーネントを参照していると影響を受けやすいため安定度は下がり、多くのコンポーネントから参照されていると変更しにくくなるため安定度は上がる。

不安定さを表す式は「I = 同コンポーネントに依存しているコンポーネント数 ÷ (同コンポーネントが依存している外部コンポーネント数 + 同コンポーネントに依存しているコンポーネント数 ) ※0が安定、1が不安定」

多数のコンポーネントがある場合、各コンポーネントが依存するコンポーネントは自分より安定度の高いものであるべきである。 依存関係逆転の原則(DIP)を使うと安定度が高い新たなコンポーネントを生み出すことができる。

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

コンポーネントの抽象度はその安定度と同程度であるべきである。 依存性関係逆転の原則(DIP)はクラスの関係を表す原則であり抽象クラスとそれ以外に区別するが、安定依存の原則(SDP)と安定度・抽象度等価の原則の組み合わせはコンポーネントの関係を表す。コンポーネントでは抽象コンポーネントか否か・安定しているか否かを明確に区別できない。

抽象度を表す式は「A = コンポーネント内のクラスの総数 ÷ コンポーネント内の抽象クラス・インターフェイス数の総数 ※0は抽象、1は具象」

第15章 アーキテクチャとは?

アーキテクチャの形状の目的は開発・デプロイ・運用・保守を容易にすること=ライフサイクルをサポートすることである。 そのための戦略は、長い期間に多くの選択肢を残すことである。 (正しく動作させることが目的ではない。その目的が達成できていたとしても開発・デプロイ・運用・保守のトラブルが多いシステムもある)

前章までの記載の通り、ソフトウェアの価値には「振る舞い」「構造」の2つがあり、アーキテクトはシステムをソフトに保つために「構造」を重視している。 アーキテクチャはシステムをソフトに保つために「方針」を重視し、「詳細」は選択肢として決定を後に伸ばす。それが良いアーキテクチャである。

第16章 独立性

優れたアーキテクチャは水平・垂直に独立している。 水平の分割はUI・ビジネスルール・DBなどのレイヤー、垂直の分割はユースケース。 独立することにより、独立した開発が可能になる。 よくある罠として、本物の重複ではなく偶然の重複によるコードを共通化してしまうことがある。偶然の重複の場合、変更頻度や変更理由が異なるために将来的に別のコードになる。 たまたまDBのテーブルのデータ構造をそのまま画面に表示する画面があった場合なども同様で、偶然の一致である可能性が高いため一致していないケースと同様にビューモデルを作成すべきである。 切り離し方の方式としてはソースコードレベル、デプロイレベル、サービスレベルがある。 方式を適用するにあたり、プロジェクト初期段階では判断が難しい。システムは成長するという考えのもと、状況に応じてソースコード、デプロイ、サービスのように切り離しのレベルを上げていくと良い。 求められる切り離し方式は時間と共に変化するため、いざという時に切り離し方式を変更できる選択肢を残すことが良いアーキテクチャである。

第17章 バウンダリー:境界線を引く

コンポーネント単位で疎結合にするために境界線を引く。 目的は、ビジネス要件と無関係のDBやサーバ構成、ライブラリといった決定を後回しにすることにある。 先にサーバ構成を決定し、マイクロアーキテクチャを適用したが、実際は必要なかった(必要ないのに修正コストが高いシステムとしてできあがってしまった)例などが挙げられている。 クリーンアーキテクチャが推奨するのは、ビジネスルールに対してデータベースとUIのコンポーネントが夫々依存している形。 ビジネスルールの変更なく、データベースとUIの変更ができる(プラグイン使える)状態である。 システムのコアであるビジネスルールから見てIOは無関係のものであるため、IOの修正を起因としたビジネスルールの修正はあってはならない。 上記の3つのコンポーネントの境界線の引き方は単一責任の原則(SRP)、依存関係逆転の原則(DIP)、安定度・抽象度依存の原則(SAP)に則っている。

第18章 境界の解剖学

境界線の引き方はいくつかある。ソースコードレベルではコンポーネント間で境界線を引く。 DLLやjarのようにデプロイレベルの境界線も存在する。 SOAやマイクロサービスといったサービスレベルの境界線もある。 サービスレベルにまで境界線が引かれると、レイテンシが発生するため注意が必要。

第19章 方針とレベル

入出力に関するコンポーネントを下位コンポーネント、遠いもの(ビジネスルール)を上位コンポーネントと呼ぶ。 同じ理由・タイミングによって決定される方針毎にコンポーネントを分割しておく。 コンポーネント間のデータフローと依存方向は必ずしも一致しない。 上位レベルの方針変更の方が下位レベルの方針よりも変更頻度が低く、重要度が高い。 UIの細かい変更などは頻繁に発生するが、ビジネスルールが変更されることは少ないという意味であり、細かいUI変更の度にビジネスルールが記載されたコンポーネントを修正するべきではない。 この議論には単一責任の原則(SRP)、オープン・クローズドの原則(OCP)、閉鎖性共通の原則(CCP)、依存関係逆転の原則(DIP)、安定依存の原則(SDP)、安定度・抽象度等価の原則(SAP)が混在する。

第20章 ビジネスルール

ビジネスルールは何にも依存させない。プラグイン(UIやDB)がビジネスルールへ依存する形にする。(第19章における上位コンポーネント=ビジネスルール となる)

ユースケースはアプリケーション固有のビジネスルールが記述される。UIの種類(Web、シンクラ、コンソール等)などは言及されない。 エンティティ(DDDのでいうドメインクラス)をUIで使用されるリクエスト・レスポンスとして使用してはいけない。作成した時には使用するデータが同一かもしれないが、その後は別のデータになる可能性が高い。変更の理由が異なるため、閉鎖性共通の原則(CCP)と単一責任の原則(SRP)に則って別のクラスにすべきだ。

第21章 叫ぶアーキテクチャ

良いアーキテクチャはどんなシステムかがわかりやすい。パッケージ構成などを見てどんなシステムか予想がつく。 アーキテクチャの目的は前述のDB・UI等の決定を後回しにすることに加え、フレームワークの決定も後回しにすることも挙げられる。 アーキテクチャはフレームワークに基づくわけではなくユースケースに基づく。 ビジネスルール(DDDでいうドメインクラス群)を先に作成し、フレームワークに依存しない形で単体テストも実行できる。 コアとなるビジネスルールを作った後に、後回しにしていたFW・DB・UI等を決定する。

第22章 クリーンアーキテクチャ

クリーンアーキテクチャはヘキサゴナルアーキテクチャ・オニオンアーキテクチャと同様に、内側に対してのみ依存することができる。 21章までに述べたプラクティスを全て実践するための方法がクリーンアーキテクチャ。

第23章 プレゼンターとHumble Object

Humble Objectパターンを使用することでテスト容易性が向上する。 Humble Objectパターンは、テストしにくい振る舞いとテストしやすい振る舞いを分リするデザインパターンである。 クリーンアーキテクチャでは、プレゼンターとビュー、データベースゲートウェイ(InteractorとDataAccess)、DataMapperなどが該当する。

第24章 部分的な境界

境界を作るとコストがかかる。Boundaryインターフェイス・InputとOutputのデータ構造クラス・夫々をコンパイル/デプロイする依存性管理等がコスト増大の原因となる。 アーキテクトは状況に応じて完全な境界に至る前の代理として部分的な境界を適用する(その後は劣化したり、完全な境界にランクアップさせたりする)。 部分的な境界の1つ方法としては、独立したコンパイルやデプロイが可能なコンポーネントの設定をし、同じコンポーネントにまとめる方法がある。バージョン管理やリリース管理の負担がなくなる。多少のコスト減少にしかならないため注意。 もう1つの例としては境界を片方にしか作らないことだ。コードを見て境界があることは明確であるが、実装次第では境界を突破されるため、開発者がルールを守る必要がある。 もう1つの例としてはFacadeパターンである。これもコードを見て境界があることは明確であるが、開発者がルールを守る必要がある。

第25章 レイヤーと境界

コンポーネントの種類は多く、様々な場所に境界は存在する。 境界を完全に構築するとコストは高くなり、オーバーエンジニアリングは悪質というYAGNIの考え方もある。 境界を完全に実装するか部分的に実装するか、実装しないか、アーキテクトが未来を予想して決定する必要がある。そして常にシステムの進化に応じて再決定を下す必要がある。

第26章 メインコンポーネント

Mainクラスはプラグインである。他の上位コンポーネント全てに依存する。 そのため、起動時のモード(プロダクション、開発、テストなど)によって設定値を変えるなどもできる。

第27章 サービス:あらゆる存在

SOAやマイクロサービスのようにサービス単位でシステムを分けるアーキテクチャがある。 サービス同士が疎結合になっていると言われるが、間接的には相互に結びついていることも多い。 SOLID原則に則って実装することで、新機能を追加する際にjarファイル(派生クラス)を追加ロードするだけで機能させることもできる。 アーキテクチャの境界はサービスとサービスの中間ではなく、サービスを横断して存在する。 サービスがアーキテクチャの境界を定義するのではなく、サービス内部のコンポーネントが定義している。

第28章 テスト境界

テストはシステムと結合しすぎてはいけない。 例えばGUIのような頻繁に変更されるものに依存しないべきである。 直接ビジネスルールのテストができるようなAPIがあると良い。

第30章 データベースは詳細

データベースは下位レベルであるため、アーキテクチャ検討時には考慮しないべきだ。 データの保存形式であるデータモデルは検討すべきであるので混同しないように。 システム上ではRAMに読み込んだ際にリスト、セット、ツリーなどのデータ構造に変換しており、ファイルやテーブル形式はDBに保存するためだけの状態だ。 パフォーマンスについてもDB独自の問題であるためアーキテクチャと切り離して考えることができる。

第31章 ウェブは詳細

UIはクリーンアーキテクチャの絵で最も外側にある。 ウェブやデスクトップアプリのようなGUIだろうがCUIだろうがアーキテクチャとして見るとUIは入出力デバイスの一種である。 デバイスに依存するようなやり取りも存在するが、ユースケースにあたる部分は抽象化してUIに依存しない実装ができる。 いくつかのアクションを経て入力データが完成した後、ユースケースが実行できる処理に至るようなイメージ。

第32章 フレームワークは詳細

フレームワークに依存しないこと。 将来的にフレームワークが提供する機能では不足する可能性がある。また、フレームワークの方向性がシステムの求める方向性と異なる可能性もある。 何かある毎に、他の全てを捨ててフレームワークを優先しなければならなくなる。 別のフレームワークに乗り換えたいと思った時にフレームワークに依存していない状態であるべき。

まとめ

一通り読みましたがやはり話題になっているSOLID原則が肝。さらに上位概念としてコンポーネント単位のSOLID原則のような説明がありましたが、ベースになるのはSOLID原則。
ということでSOLID原則は絶対に暗記すべきです。わりと現場でも使う言葉になっているし、YAGNI並に知っていて当然の言葉になってきている。

この本を読む人の大半はクリーンアーキテクチャについて学びたい人だと思うが、各章の情報をまとめたものがクリーンアーキテクチャになるためやはり一通り読んだ方が良いと思う。
書籍に記載されたパッケージ構成をそのまま厳密に再現する必要はないと思うが、ここまでやると厳密に依存性が低いコンポーネントを切り替えできる状態になるようだ。
YAGNIをベースにビジネスロジックが他のコンポーネントに依存しないようにコンポーネント間の依存関係を制御することを肝とするだけでシステムのメンテナンス性は大幅に向上すると記載されていたが、DDDを実践していて(ビジネスロジックをドメイン層に閉じ込めることで)メンテナンス性が高くなることは大いに感じたし、クリーンアーキテクチャまでやるか否かは別としてもビジネスロジックを閉じ込めることは最重要であることはよくわかった。