概要
50年近い(!)開発経験を持つ通称「アンクル・ボブ」が、時代が変わっても変わらないアーキテクチャのルールについて書いた本です。
読もうと思ったきっかけ
これまでやってきたプロジェクトでは、Djangoのような縛りの強いフレームワークに沿ってコードを書いていたため、アーキテクチャについて考える機会がありませんでした。
6月から参加しているRustのプロジェクトでは、そういったものがなく、自由に書けてしまうので、アーキテクチャについても意識していないとよくないコードを書いてしまうと思いました。
先輩にこのことを相談したところ、こちらの本を勧められたので読んでみることにしました。
要約の要約
この後に章ごとの要約が続きますが、同じことを少しずつ言い方を変えて何度も言っているタイプの本なので、先に全体の要約を書いておきます。
- アーキテクチャとは何か?
- 言葉の響きからして上位レイヤーのものを想像するかもしれないが、「設計」と同義だと考えてよい。
- ソフトウェアシステムの価値は、「アーキテクチャ」と「振る舞い」に大別でき、アーキテクチャの方が振る舞いより重要である。
- 振る舞いとは何か?
- 動作のこと。入力に対して正しい出力が返ってくれば、正しい振る舞いをしているといえる。
- 振る舞いは箱の外側、アーキテクチャは箱の内側、というイメージ。
- アーキテクチャの目的は何か?
- システムの構築・保守に必要な工数を最小限に抑えること。
- 具体的には、追加や変更に強く、テストが容易なシステムにすること。
- クリーンアーキテクチャとは何か?
- 方針(ビジネスロジック、重要なもの)を中心、詳細(DBやUI、重要でないもの)を外側に置き、依存性の矢印を外から内に向けた同心円の図で表せるアーキテクチャ。
- 依存性の矢印の方向を制御するために、インターフェイスや抽象クラスが使われる。
- なぜクリーンアーキテクチャがよいのか?
- 変更の理由ごとにモジュールが分割されているので、変更に強い。
- 変更の頻度が高い(重要でない)ものほど円の外側にある(被依存が少ない)ので、変更に強い。
要約
序文
- 50年前と今を比べると、ハードウェアには劇的な進化があったが、コードの書き方はほとんど変わらない。
- であれば、ソフトウェアアーキテクチャのルールも50年前と同じだと言えるだろう。
第Ⅰ部 イントロダクション
第1章 設計とアーキテクチャ
- コードを1度だけ動かすことは簡単だが、コードを「正しく」することは格段に難しい。
- ソフトウェアアーキテクチャの目的は、構築・保守のための人材を最小限にすることである。
- クリーンなコードを書くのは、汚いコードを書くよりも時間がかかると思われがちだが、実際はそうではない。
第2章 2つの価値のお話
- ソフトウェアの価値は振る舞いと構造(アーキテクチャ) である。
- ソフトウェアは、その名前の通り、ソフト(変更が容易)であるべきである。
- アイゼンハワーのマトリックス(「緊急」「重要」の2軸の表)を例にとると、
- 「振る舞い」は「緊急だが重要でない」
- 「構造」は「重要だが緊急でない」
- だが、「振る舞い」の重要度が高く見積もられ、「構造」の重要度が低く見積もられがち。
- 開発者は、「構造」の重要度をステークホルダーに強く主張していく必要がある。
第Ⅱ部 構成要素から始めよ:プログラミングパラダイム
第3章 パラダイムの概要
- 構造化プログラミング
- オブジェクト指向プログラミング
- 関数型プログラミング
- これらは、「何をすべきでないか」を示すものである。
第4章 構造化プログラミング
- テスト可能な単位まで機能を分割すること。
- 機能分割を困難にするgoto文は、歴史の中で消えていった。
第5章 オブジェクト指向プログラミング
- オブジェクト指向の最も優れた点はポリモーフィズムである。
- ポリモーフィズムを活用すると、ソースコードの依存関係を逆転させることができる。
- 通常、上位モジュールが下位モジュールの処理を呼び出す場合、上位モジュールから下位モジュールへの参照が発生する。
- しかし、以下の方法で、依存関係を逆転させることができるようになる。
- 上位モジュールと同じ階層にインターフェイスを宣言し、上位モジュールはインターフェイスの処理を呼び出す。
- 下位モジュールはインターフェイスを参照し、それに沿って処理を実装する。
第6章 関数型プログラミング
- 関数型プログラミングで重要なのは不変性である。
- 変数が不変であることは、競合、デッドロック、並行更新といった問題の解決になる。
- ハードウェアの進化により、ストレージと処理能力の制限がほとんどなくなったため、イベントソーシングのような考え方も現実的になった。
- イベントソーシングでは、状態ではなく取引を保存し、状態が知りたければ全ての取引を合計する。
- データストアに対する変更はCRUDにおけるCRだけになる。
- ソースコード管理システムがその代表例。
第Ⅲ部 設計の原則
- SOLID原則は、モジュールレベル(コードレベルの1つ上)の開発の思想。
第7章 単一責任の原則(SRP)
-
「モジュールを変更する理由はたったひとつであるべきである。」
- 言い換えれば、「モジュールはたったひとつのアクターに対して責務を負うべきである。」
- 具体的には、データを関数から切り離し、Facadeパターンでまとめあげるなど。
第8章 オープン・クローズドの原則(OCP)
- 「ソフトウェアの構成要素は拡張に対しては開いていて、修正に対して閉じていなければならない。」
- 下位コンポーネントに変更が入るとき、上位コンポーネントへの変更が必要ないようにする。
- そのためにインターフェイスを用いて依存関係を制御する。
第9章 リスコフの置換原則(LSP)
- 「ここで望まれるのは、次に述べるような置換可能な性質である:S型のオブジェクトo1の各々に、対応するT型のオブジェクトo2が1つ存在し、Tを使って定義されたプログラムPに対してo2の代わりにo1を使ってもPの振る舞いが変わらない場合、SはTの派生型であると言える。」
- 継承を使う場合、継承先(例:BusinessLicense)は継承元(例:License)と置換可能であるべきである。
- 正方形は、長方形の派生型とはいえない。
- 最初はコードレベルの原則だったが、今では適用範囲が広がり、ソフトウェア設計の原則になっている。
第10章 インターフェイス分離の原則(ISP)
-
使わない機能に依存するべきではない。
- 使わない機能に依存していると、その機能を変更した時に、その機能を使わない部分まで再コンパイル、デプロイが必要になってしまうから。
- この点では、静的型付け言語よりも動的型付け言語の方が疎結合な実装をしやすい。
- ソースコードレベルに限らず、アーキテクチャレベルでも同じことが言える。
第11章 依存関係逆転の原則(DIP)
- (具象ではなく)抽象だけを参照するべきである。
- そのための実装としては、Abstract Factoryパターンが代表的である。
第Ⅳ部 コンポーネントの原則
第12章 コンポーネント
- コンポーネントとは、デプロイの単位である。
- 歴史の話
- PDP-8のような言語では、アプリケーションからライブラリをロードする際のメモリ確保に課題があった。
- それを解決したのがリロケータブル(再配置可能)なリンクローダだった。
- 60〜70年代、「マーフィーの法則」によってプログラムの大きさは肥大化していき、リンクローダの処理が重すぎて使えない状態になったため、リンクの処理をリンカに任せるようになった。
- 80年代後半には、マーフィーの法則を上回る速度で「ムーアの法則」によってコンピュータの処理速度が向上し、現在では数秒でリンク処理ができるようになった。
第13章 コンポーネントの凝集性
- 再利用・リリース等価の原則(REP)
- 再利用の単位とリリースの単位は等価になる。
- 閉鎖性共通の原則(CCP)
- 同じ理由、同じタイミングで変更されるクラスをコンポーネントにまとめること。変更の理由やタイミングが異なるクラスは、別のコンポーネントに分けること。
- SRPのコンポーネント版。
- 全再利用の原則(CRP)
- コンポーネントのユーザーに対して、実際には使わないものへの依存を強要してはいけない。
- ISPのコンポーネント版。
- この3つの原則には相反する部分があり、プロジェクトの特性や進み具合によって適切にバランスを取ることが必要になる。
第14章 コンポーネントの結合
- 非循環依存関係の原則(ADP)
- コンポーネントの依存グラフに循環依存があってはいけない。
- コンポーネントを分割して依存関係を明確にすると、とりわけ複数人で開発を行う場合、マージがしやすくなる。
- 循環参照が発生している箇所は、DIPを適用して依存関係を逆転させる。
- 安定依存の原則(SDP)
- 安定度の高い方向に依存すること。
- 変更が少ないコンポーネントから、変更が多いコンポーネントへの参照は避けるべき。
- 不安定さを
依存出力数 / (依存入力数 + 依存出力数)
で算出し、この値が高い方から低い方に依存の矢印が向くように設計するのが望ましい。
- 安定度・抽象度等価の原則(SAP)
- コンポーネントの抽象度は、その安定度と同程度でなければいけない。
- 安定度の高いコンポーネントは、インターフェイスや抽象クラスを用いて作る。
第Ⅴ部 アーキテクチャ
第15章 アーキテクチャとは?
- アーキテクチャの主な目的は、システムの開発・デプロイ・運用・保守を容易にし、ひいてはプログラマの生産性を最大にすることである。
- あらゆるソフトウェアシステムは、「方針(ビジネスロジック)」と「詳細(インターフェイス)」に分割でき、システムの本当の価値を持つのは「方針」である。
- アーキテクトの目的は、方針と詳細を分離し、詳細の決定を延期・留保することである。
- 方針と詳細が分離されていると、複数の詳細を試せる、デバイスの交換が楽などのメリットがある。
第16章 独立性
- システムを適切に切り離すことで、以下のことをサポートすることができる。
- システムのユースケース
- システムの運用
- システムの開発
- システムのデプロイ
- レイヤーは、UI、ビジネスロジック、DBのように、変更する理由の単位で切り離す。
- ユースケースは、水平に分けたレイヤーを垂直にスライスしたものである。
- ユースケースもやはり、変更する理由の単位で切り離す。そうすると、新しいユースケースの追加が古いユースケースに影響を及ぼすことを防げる。
- 切り離しのメリット
- 運用においても役に立つ。
- 高いスループットで実行しなければいけないものと低いスループットで実行したいものを分離しておけば、それぞれを異なるサーバーに配置して、通信でやりとりするという方法を採れる。このようなコンポーネントは「サービス」「マイクロサービス」と呼ばれ、このようなアーキテクチャは「サービス指向アーキテクチャ」と呼ばれる。
- 開発において、チーム同士の干渉を減らすことができる。
- デプロイの柔軟性が高まる。
- 運用においても役に立つ。
- 重複は排除するべきだが、変更する理由が違うのに似ている「偶然の重複」には注意するべき。
第17章 バウンダリー:境界線を引く
- 「重要なもの」と「重要でないもの」(例えば、ビジネスロジックとデータベース)の間に境界線を引くことで、重要でないものの決定を遅らせることができる。
- GUIこそがシステムだという勘違いが起こりがちだが、GUIはあくまで入出力であり、ビジネスロジックとは無関係。
- ビデオゲームにおいてすら、ビジネスロジックにとってGUIは重要ではない。
- 具体案として、中心にあるビジネスロジックに対して、GUIやDBをプラグインとして追加する「プラグインアーキテクチャ」が挙げられる。
第18章 境界の解剖学
- 境界にはいくつかの種類がある。
- ソースレベルの境界、デプロイコンポーネントの境界は、越えるのにかかるコストが安価である。
- 一方、ローカルプロセスの境界、サービスの境界を越えるのはハイコストのため、量は最低限にした方がよい。
- いずれにせよ、依存性の方向は下位コンポーネントから上位コンポーネントに向いているべきである。
- 上位コンポーネントのソースコード内に下位コンポーネントの情報(プロセス名、URIなど)を含めるべきではない。
第19章 方針とレベル
- 「上位(下位)レベルのコンポーネント」の「レベル」は、「入力と出力からの距離」と定義できる。
- 例えば、シンプルな暗号化のプログラムを考えたとき、文字を読み取る処理、書き出す処理は下位レベルで、暗号化アルゴリズムに沿って文字を変換する処理は上位レベルになる。
第20章 ビジネスルール
- ビジネスマネーを生み出したり節約したりするためのルールを最重要ビジネスルールと呼ぶ。
- 最重要ビジネスルールに必要なデータを最重要ビジネスデータと呼ぶ。
- 銀行のローンの例だと、ローンに利子をつけることが最重要ビジネスルールで、貸付金残高、金利、支払いスケジュールなどが最重要ビジネスデータである。これらは、システムが自動化されているかどうかとは関係なく存在する。
- 最重要ビジネスルールと最重要ビジネスデータを結びつけるオブジェクトをエンティティと呼ぶ。
- エンティティは、データベース、UIなどのことを気にせずに作る。
- (システム化以前の)手動の環境では必要ないような、アプリケーション固有のビジネスルールをユースケースと呼ぶ。
- ユースケースは、エンティティの最重要ビジネスルールを呼び出すかを規定したルールを含む。
- ユースケースはエンティティより下位のレベルだが、入出力とは無関係である。
- ユースケースの入力と出力のモデルは、構造が似ているのでエンティティに依存させたくなるが、そうしてはならない。目的が違っており、違う理由で変化するからだ。
第21章 叫ぶアーキテクチャ
- 戸建や図書館の設計図が建物のユースケースを叫んで(表現して)いるように、ソフトウェアのアーキテクチャもそのユースケースを叫ぶべきだ。
- 一目見て、何のシステムか分かるようなアーキテクチャが望ましい。
- アーキテクチャはフレームワークに依存するべきではない。そうするとユースケースに依存することができなくなる。
- ユースケースに基づいたアーキテクチャであれば、Webサーバーやデータベースを立ち上げることなくテストができる。
第22章 クリーンアーキテクチャ
- 同心円の図
- ソースコードの依存性は、内側(上位レベルの方針)だけに向かっていなければならない。
- 中心にあるのはエンティティ。企業の最重要ビジネスルールをカプセル化したものである。
- エンティティの次がユースケース。アプリケーション固有のビジネスルールが含まれる。
- 次の層にはインターフェイスアダプターがある。ユースケースやエンティティのデータ形式と、データベースやUIに向けた形式を相互に変換する。
- 最も外側の円は、フレームワークやデータベースである。この層にコードを書くことは通常あまりない。
- この4つのレイヤーだけで構成されている必要はない。いずれにせよ、依存性の方向のルールが守られていることが重要である。
第23章 プレゼンターとHumble Object
- Humble Objectパターンは、テストがしやすい振る舞いと難しい振る舞いに分けるデザインパターンである。
- 例えば、GUIをPresenterとViewの2つのクラスに分ける。
- Presenterは、アプリケーションから値を取ってきて、表示用にフォーマット(Dateオブジェクトを文字列にするなど)してViewModelに格納する。
- Viewは、ViewModelから値を読み込んで画面に表示するだけ。Humble(控えめ)である。
- UIのテストは難しいが、Presenterのテストはずっと簡単である。
- データベースやサービスとの境界にも、同じようにHumble Objectパターンを用いて、テストを簡単にすることができる。
第24章 部分的な境界
- コンポーネント間に完全な境界を引くのはコストが高い。
- コストを下げるには、コンポーネントを分けつつも、コンパイルやデプロイの単位としては一つにまとめてしまい、部分的な境界を実装する方法がある。
- そのほか、Strategyパターン、Facadeパターンを採用して部分的な境界を実装する手もある。
- これらはコンポーネントの分離を劣化させうる可能性を孕んでいるため、注意が必要である。
第25章 レイヤーと境界
- 潜在的な境界はあらゆるところに存在する。アーキテクトはそれらを見越してアーキテクチャを設計する必要がある。
- これはYAGNIの哲学に反するが、後からレイヤーや境界を追加するコストがとても高いことを考えると、無視していいものではない。
第26章 メインコンポーネント
- Mainコンポーネントは、最下位レベルの詳細である。
- 具体的には、アプリケーション本体で把握する必要のない文字列を定義したり、依存性の注入を行ったりする。
- 初期状態や構成を設定して、外部リソースを集め、上位レベルの方針に制御を渡すプラグインと考えるとよい。
第27章 サービス:あらゆる存在
- サービスを使用するアーキテクチャは、サービス同士が分離されている、開発やデプロイが独立しているように見えるが、それが正しくない場合もある。
- 例えば、サービス間でやり取りするレコードにフィールドを追加することになったとき、全てのサービスに変更を加える必要が出てくる。
- モノリシックなシステムであっても、コンポーネントがしっかり分離されていれば、追加や変更に強くできる。
第28章 テスト境界
- テストはシステムの一部だ。
- テストはシステムに依存しているので、円の最も外側の詳細である。
- 少しの変更で壊れるような脆弱なテストは、システムを硬直化させてしまう。
- これを防ぐためには、テストをGUIのような変化しやすいものに依存させないことだ。
- 具体的には、テストから使用するAPIを作成する。
- テストAPIでは、UIやデータベースをバイパスし、ビジネスルールのみをテストするようにする。
第29章 クリーン組込みアーキテクチャ
- 組込みの開発においても、基本的にやることは同じである。
- ハードウェアは詳細なので、ソフトウェアはハードウェアに依存するべきではない。
- ハードウェアに依存するコードはファームウェアである。ファームウェアは量を必要最低限に抑えるべきである。
- そうしておくと、ハードウェアの入れ替えが容易になるし、ターゲットハードウェア以外でもテストできるようになる(ソフトウェアはハードウェアより寿命が長い傾向にあるので、そうあるべきだ)。
- ソフトウェアとファームウェアの間にはハードウェア抽象化レイヤー(HAL: Hardware Abstraction Layer)と呼ばれる境界を設ける。
- 同様に、OSも詳細なので、ソフトウェアとOSの間にはOS抽象化レイヤー(OSAL: Operating System Abstraction Layer)を設ける。
第Ⅵ部 詳細
第30章 データベースは詳細
- データベースは、ディスクとRAMとの間でデータを移動する仕組みにすぎない。
- もし今後技術の進歩でディスクがなくなって、全てのデータをRAMに保存できるようになったら、リスト、セット、スタック、キューのような扱いやすい形式が使われ、テーブル形式は使われなくなるであろうことがその証拠だ。
- 重要なのはデータモデルであり、データベースは詳細である。
第31章 ウェブは詳細
- ウェブの登場は何もかもを変えてしまったように見えるが、本質は何も変わっていない。
- 計算能力を中央サーバーに持たせるか端末に持たせるかの振り子は、ウェブの登場前から同じように行ったり来たりしている。
- ウェブは入出力デバイスの一種である、という捉え方ができる。
第32章 フレームワークは詳細
- フレームワークのアーキテクチャには難があることが多く、例えばフレームワークのクラスを継承してエンティティを作ることなどは、依存性のルールに反している。
- フレームワークを使うことは構わないが、結合してはいけない。
第33章 事例:動画販売サイト
- はじめにすることは、アクターとユースケースを見つけることだ。
- このシステムの場合は、管理者、動画の作者、購入者、視聴者というアクターがそれぞれ、価格の登録、動画の登録、ライセンスの購入、動画のストリーミング視聴といったユースケースを持つことになる。
- これをもとにコンポーネントアーキテクチャを検討する。
- 今回は、各アクターごとにView, Presenter, Interactor, Controllerを分けつつ、購入者のカタログ閲覧と視聴者のカタログ閲覧を共通化したViewとPresenterも設けた。
- アクターごとの分離は単一責任の原則(SRP)によるもので、View, Presenterのような分離は依存性のルールによるものだ。
- デプロイの単位は、この時点では確定させなくてもよい。各コンポーネントを別々にデプロイしてもよいし、2つのjarファイル(Javaの場合)にまとめることもできる。
第34章 書き残したこと
- 悪魔は実装の詳細に宿るものだ。 いかに優れた設計であっても、実装方法の複雑さが考慮されていなければいけない。
- クリーンアーキテクチャの原則を守っても、例えば全てのクラスが
public
で宣言されていたら、パッケージは単なるフォルダ分け程度の意味しか持たず、カプセル化ができない。
感想
抽象的な話が延々と続くので頭を使いましたが、面白かったです。
システムに限らず、組織とか個人の人生とかにも通じるものがあるなーと思いながら読んでいました。
同じ話題が何度も出てくるのでちょっとうんざりしました(章ごとの要約を書いてなかったら飛ばしてたかも)が、それだけ重要なことなのでしょう。
定期的に昔話とか、「著者の好き嫌いの話では?」という話(フレームワークのところとか)が出てきます。これは正直流し読みでいいと思います。
前提としている技術が全体的に古いので、令和の開発にそっくりそのまま反映できるかというと疑問です。あくまで抽象的な思想を学ぶ本ですね。
コードの書き方としてのクリーンアーキテクチャを採用せずとも、システムに関わる人間として役に立つ考え方がたくさん詰まっていて、一つレベルアップできそうな気がします。
今業務で作っているシステムはフロントとサーバーが分かれていて、これのサーバーサイドだけにクリーンアーキテクチャをそのまま当てはめるのは難しそうというか、本質と違う気もするので、実践にあたってはまた頭を使うことになりそうです。