第Ⅰ部 イントロダクション
第1章 設計とアーキテクチャ
ソフトウェアアーキテクチャの目的は、求められるシステムを構築・保守するために必要な人材を最小限に抑えることである。
…事実は、短期的にも長期的にも崩壊したコードを書くほうがクリーンなコードを書くよりも常に遅い。
■まとめ
いずれの場合も、開発組織が自らの自信過剰を認識して回避し、ソフトウェアアーキテクチャの品質と真剣に向き合うことが、最善の選択肢となる。
ソフトウェアアーキテクチャと真剣に向き合うには、優れたソフトウェアアーキテクチャとは何かを知る必要がある。労力の最小化と生産性の最大化を実現する設計とアーキテクチャを構築するには、そこに到達するためのシステムアーキテクチャの属性を把握する必要がある。
それが本書の内容だ。優れたクリーンなアーキテクチャと設計がどのようなものかを説明する。本書を読んだソフトウェア開発者は、長期的に利益をもたらすシステムを構築できるだろう。
第2章 2つの価値のお話
…ソフトウェア開発チームには、機能の緊急性よりもアーキテクチャの重要性を強く主張する責任が求められる。
第Ⅱ部 構成要素から始めよ:プログラミングパラダイム
第3章 パラダイムの概要
構造化プログラミングは、直接的な制御の移行に規律を課すものである。
オブジェクト指向プログラミングは、間接的な制御の移行に規律を課すものである。
関数型プログラミングは、代入に規律を課すものである。
■まとめ
パラダイムの歴史的な教訓は、アーキテクチャとどのように関係しているのだろうか?「すべて」において関係している。我々は、アーキテクチャの境界を越えるための仕組みとして、ポリモーフィズムを使う。我々は、データの配置やアクセスに規律を課すために、関数型プログラミングを使う。我々は、モジュールのアルゴリズムの基盤として、構造化プログラミングを使う。
これらの3つが、アーキテクチャの3つの大きな関心事に対応していることに注目してほしい。その3つとは「コンポーネントの分離」「データ管理」「機能」である。
第4章 構造化プログラミング
■まとめ
構造化プログラミングの価値を高めるのは、反証可能なプログラミングの単位を作成する能力である。したがって、現代の言語では無制限のgoto文がサポートされていない。このことでは、アーキテクチャレベルにおいて、機能分割がベストプラクティスだと考えられている理由でもある。
最小の機能から最大のコンポーネントまで、あらゆるレベルにおいて、ソフトウェアは科学のように、反証可能性によって動かされている。ソフトウェアアーキテクトは、簡単に反証できる(テスト可能な)モジュール、コンポーネント、サービスを定義しようとする。そのために、さらに上位のレベルにおいて、構造化プログラミングのような制限を課している。
こうした制限については、今後の章で詳しく調べていきたい。
第5章 オブジェクト指向プログラミング
■まとめ
「OOとは何か?」この質問には、多くの意見と多くの答えがある。だが、ソフトウェアアーキテクトにとって、その答えは明らかだ。00とは「ポリモーフィズムを使用することで、システムにあるすべてのソースコードの依存関係を絶対的に制御する能力」である。これにより、アーキテクトは「プラグインアーキテクチャ」を作成できる。これは、上位レベルの方針を含んだモジュールを下位レベルの詳細を含んだモジュールから独立させることである。下位レベルの詳細はプラグインモジュールとなり、上位レベルの方針を含んだモジュールとは独立して、デプロイおよび開発することが可能となる。
第6章 関数型プログラミング
■まとめ
これまでの流れをまとめよう。
●構造化プログラミングは、直接的な制御の移行に規律を課すものである。
●オブジェクト指向プログラミングは、間接的な制御の移行に規律を課すものである。
●関数型プログラミングは、代入に規律を課すものである。
これら3つのパラダイムは、我々から何かを奪っている。それぞれがコードの書き方に何らかの制限をかけている。いずれのバラダイムも我々にパワーや能力を与えてくれるものではない。過去半世紀にかけて我々が学んだのは、何をすべきではないかである。
そのために我々は、歓迎できない事実に直面する必要がある。それは、ソフトウェアは急速に進歩する技術ではないということだ。現在のソフトウェアのルールは、Alan Turingが電子計算機で実行するための最初のコードを書いた1946年のものと同じである。ツールは変わり、ハードウェアも変わったが、ソフトウェアの本質は変わっていない。
ソフトウェア(コンピュータプログラムの本質)は、「順次」「選択」「反復」と「間接参照」で構成されている。それ以上でもそれ以下でもない。
第Ⅲ部 設計の原則
SOLID原則の目的は、以下のような性質を持つ中間レベルのソフトウェア構造を作ることだ。
・変更に強いこと
・理解しやすいこと
・コンポーネントの基盤として、多くのソフトウェアシステムで利用できること
■SOLID原則
●単一責任の原則(SRP Single Responsibility Principle)
コンウェイの法則から導かれる当然の帰結。個々のモジュールを変更する理由がたったひとつだけになるように、ソフトウェアシステムの構造がそれを使う組織の社会的構造に大きな影響を受けるようにする。
●オープン・クローズドの原則(OCP Open-Closed Principle)
Bertrand Meyerが80年代に広めた原則。ソフトウェアを変更しやすくするために、既存のコードの変更よりも新しいコードの追加によって、システムの振る舞いを変更できるように設計すべきである。
●リスコフの置換原則(LSP Liskov Substitution Principle)
Barbara Liskovが提唱した有名な派生型の定義。1988年に誕生。要するに、交換可能なパーツを使ってソフトウェアシステムを構築するなら、個々のパーツが交換可能となるような契約に従わなければいけないということ。
●インターフェイス分離の原則(ISP Interface Segregation Principle)
ソフトウェアを設計する際には、使っていないものへの依存を回避すべきだという原則。
●依存関係逆転の原則(DIP Dependency Inversion Principle)
上位レベルの方針の実装コードは、下位レベルの詳細の実装コードに依存すべきではなく、逆に詳細側が方針に依存すべきであるという原則。
第7章 SRP
■まとめ
単一責任の原則(SRP)は関数やクラスに関する原則だが、同じような原則が別のレベルでも登場する。コンポーネントレベルでは、この原則は「閉鎖性共通の原則(CCP)」と呼ばれるようになる。また、アーキテクチャレベルでは、「アーキテクチャの境界」を作るための「変更の軸」と呼ばれている。これらについては、すべてこの後の章で取り上げる。
第8章 OCP
■まとめ
オープンクローズドの原則(OCP)は、システムのアーキテクチャの隠れた原動力である。その目的は、変更の影響を受けずにシステムを拡張しやすくすることだ。目的を達成するために、システムをコンポーネントに分割して、コンポーネントの依存関係を階層構造にする。そして、上位レベルのコンポーネントが下位レベルのコンポーネントの変更の影響を受けないようにする。
第9章 LSP
■まとめ
リスコフの置換原則(LSP)はアーキテクチャのレベルにも適用できる。むしろ適用すべきである。置換可能性に少しでも違反してしまうと、システムのアーキテクチャが特別な仕組みだらけになってしまう。
第10章 ISP
■まとめ
本章での教訓は、必要としないお荷物を抱えたものに依存していると、予期せぬトラブルの元につながるということだ。
この件については、第13章「コンポーネントの凝集性」で全再利用の原則(CRP)を扱う際に改めて掘り下げたいと思う。
第11章 DIP
■まとめ
本書では、ここからさらに上位レベルのアーキテクチャの原則を扱うことになるが、依存関係逆転の原則(DIP)も何度となく登場する。これは、アーキテクチャの図のなかで最も目立つ原則だろう。図11-1の曲線は、後の章では「アーキテクチャの境界」を表すことになる。曲線を横切る依存性が抽象に向かっているが、これは後の章では「依存性のルール」という新しいルールとして登場する。
第Ⅳ部 コンポーネントの原則
SOLID原則がレンガを組み合わせて壁や部屋を作る方法を伝える原則だとするならば、コンポーネントの原則は部屋を組み合わせて建物を作る方法を伝える原則である。大規模建築と同様に、大きなソフトウェアシステムは小さなコンポーネントを組み合わせて作られている。
第12章 コンポーネント
■まとめ
動的にリンクされたファイルを実行時にプラグインできる。これが我々のアーキテクチャにおけるソフトウェアコンポーネントだ。ここにたどり着くまでに大変な労力を要する50年だったが、我々は気軽に使えるコンポーネントプラグインアーキテクチャをようやく手に入れることができた。
第13章 コンポーネントの凝集性
■再利用・リリース等価の原則(REP)
再利用の単位とリリースの単位は等価になる
■閉鎖性共通の原則(CCP)
同じ理由、同じタイミングで変更されるクラスをコンポーネントにまとめること。変更の理由やタイミングが異なるクラスは、別のコンポーネントに分けること。
■全再利用の原則(CRP)
コンポーネントのユーザーに対して、実際には使わないものへの依存を強要してはいけない
■まとめ
かつての我々は、凝集性に関してそれほど深く考えることはなく、再利用・リリース等価の原則(REP)、閉鎖性共通の原則(CCP)、全再利用の原則(CRP)よりもずっとシンプルに考えていた。凝集性は「ひとつのモジュールはひとつだけの機能を持っている」という属性のことだと考えていた。しかし、コンポーネントの凝集性に関するこれら3つの原則は、それがもっと複雑なものであることを示している。どのクラスをコンポーネントにまとめるかを決めるときには、開発時の利便性と再利用性のトレードオフを考慮する必要がある。アプリケーションのニーズに従ってこれらのバランスをとるのは、簡単なことではない。また、最適な落としどころは常に変わり続ける。つまり、今の時点で適切だった判断が、翌年には不適切になっているかもしれない。プロジェクトが進み、再利用性を重視するようになると、コンポーネントの構成も変わっていくのである。
第14章 コンポーネントの結合
■非循環依存関係の原則(ADP)
コンポーネントの依存グラフに循環依存があってはいけない。
■安定依存の原則
安定度の高い方向に依存すること。
■安定度・抽象度等価の原則(SAP)
コンポーネントの抽象度は、その安定度と同程度でなければいけない。
■まとめ
本章で取り上げた依存性管理の指標は、設計における依存性と抽象度の関係が、私が「よいもの」と考えているパターンに当てはまるかどうかを計測するものである。経験上、世の中にはよい依存もあれば、悪い依存もある。これらの指標は、私のこれまでの経験を踏まえたものになっている。とはいえ、これらの指標がすべてではない。これは何らかの基準に従って計測しただけにすぎない。だが、万能ではないかもしれないが、それなりに役立つのではないだろうか。
第Ⅴ部 アーキテクチャ
第15章 アーキテクチャとは?
■まとめ
本章で紹介した2つの物語は、アーキテクトが大規模に採用している原則の小規模な活用例である。優れたアーキテクトは、方針と詳細を慎重に区別して、方針が詳細を把握することなく、決して依存することがないように、両者を切り離す。優れたアーキテクトは、詳細の決定をできるだけ延期・留保できるように、方針をデザインする。
第16章 独立性
■まとめ
そう、これは一筋縄ではいかないのだ。切り離し方式の変更は設定オプションにすべきだと言いたいわけではない(そのほうが適切なこともあるが)。私が言っているのは、システムの切り離し方式は時間とともに変化する可能性があるということだ。そして、優秀なアーキテクトであれば、そうした変化を予見して、適切に進めていくのである。
第17章 バウンダリー:境界線を引く
■まとめ
ソフトウェアアーキテクチャに境界線を引くためには、まずはシステムをコンポーネントに分割する。そのなかのいくつかのコンポーネントがコアのビジネスルールになる。必要な機能が含まれているそのほかのコンポーネントは、コアのビジネスには直接関係しないので、プラグインにしておく。次に、コンポーネントにコードを配置して、そこから一方向にコアのビジネスに向かって矢印を描く。
これは、依存関係逆転の原則(DIP)と安定度・抽象度等価の原則(SAP)を適用したものであると認識すべきだ。依存性の矢印が詳細レベルから抽象レベルを指すようになっている。
第18章 境界の解剖学
■まとめ
モノリス以外のほとんどのシステムでは、複数の境界戦略を使用する。サービスの境界を利用するシステムが、ローカルプロセスの境界を同時に利用することもある。実際サービスは相互作用する複数のローカルプロセスのファサードにすぎない。また、サービスやローカルプロセスはほぼ確実に、ソースコードのコンポーネントで構成されたモノリスか、動的にリンクされたデプロイコンポーネントのいずれかである。
つまり、システムの境界は、ローカルでにぎやかな境界とレイテンシーに影響される境界が混在しているのである。
第19章 方針とレベル
■まとめ
方針の議論には、単一責任の原則(SRP)、オープン・クローズドの原則(OCP)、閉鎖性共通の原則(CCP)、依存関係逆転の原則(DIP)、安定依存の原則(SDP)、安定度・抽象度等価の原則(SAP)が混在している。本章を読み返し、それぞれの原則がどこで使われているのか、どうして使われているのかを確認してもらいたい。
第20章 ビジネスルール
アプリケーションをビジネスルールとプラグインに分割する場合、実際のビジネスルールがどのようなものかを把握しておいたほうがいいだろう。ビジネスルールにはいくつかの種類があることがわかる。
ビジネスルールとは、ビジネスマネーを生み出したり節約したりするルールや手続きのことだ。厳密に言えば、コンピュータで実装されているかどうかにかかわらず、ビジネスマネーを生み出したり節約したりするルールのことだ。手動で実行されたとしても、お金を生み出したり節約したりすることはできる。
たとえば、銀行がローンにN%の利子を付けているとすると、それは銀行のお金を生むためのビジネスルールになる。利子をコンピュータで計算しようと、そろばんで計算しようと、まったく関係はない。
こうしたルールのことを最重要ビジネスルールと呼ぶ。ビジネスにとって欠かせないものであり、システムが自動化されていなくても存在するからだ。
最重要ビジネスルールには、いくつかのデータが必要になる。たとえば、ローンであれば、貸付金残高、金利、支払いスケジュールなどが必要になる。
こうしたデータのことを最重要ビジネスデータと呼ぶ。システムが自動化されていなくても存在するデータだからだ。
最重要ビジネスルールと最重要ビジネスデータは密接に結び付いているため、オブジェクトの有力な候補になる。こうしたオブジェクトのことをエンティティと呼びたい。
■まとめ
ビジネスルールは、ソフトウェアシステムが存在する理由である。中心的な機能である。お金を生み出したり節約したりするコードを保持したものである。いわば、家宝である。
ビジネスルールはそのままでいなければいけない。使用するユーザーインターフェイスやデータベースなど、下位の懸念事項に関わるべきではない。ビジネスルールを表すコードがシステムの心臓部となり、そこにプラグインされるものについては、何も配慮しないことが理想である。ビジネスルールはシステムのなかで、最も独立していて、最も再利用可能なコードでなければいけないのだ
第21章 叫ぶアーキテクチャ
■まとめ
アーキテクチャは、システムで使用しているフレームワークではなく、システムそのものについての情報を伝える必要がある。たとえば、ヘルスケアシステムを構築しているならば、新しく参加したプログラマがソースリポジトリを見たときに、「ああ、これはヘルスケアシステムだ」と思えるようにしておくべきである。システムの提供方法はまだわからなくても、システムのユースケースをすべて把握できるようにしておくべきだ。そして、あなたのところへやって来て、こう言うだろう。
「モデルのようなものは見えますが、ビューとコントローラーはどこにありますか?」
あなたはこう答えるだろう。
「それは詳細だから、まだ気にする必要はないよ。あとから決めよう」
第22章 クリーンアーキテクチャ
■まとめ
こうした単純なルールに従うのは、それほど難しいことではない。ルールを守っていれば、いずれ多くの苦痛から解放してくれるだろう。ソフトウェアをレイヤーに分割して、依存性のルールを守れば、本質的にテスト可能なシステムを作り、それがもたらすメリットを受け取ることができる。システムの外部のパーツ(データベースやウェブフレームワーク)が廃れたとしても、そうした要素を最小限の労力で置き換えることができる。
第23章 プレゼンターとHumble Object
■まとめ
アーキテクチャの境界の近くには、Humble Objectパターンが潜んでいる。境界を越える通信には、シンプルなデータ構造が含まれている。また、境界はテストしにくい部分とテストしやすい部分に分割する。アーキテクチャの境界でこのパターンを使用すると、システム全体のテスト容易性が大幅に向上する。
第24章 部分的な境界
■まとめ
アーキテクチャの境界を部分的に実装する3つのシンプルな方法を見てきた。もちろんほかにも方法はあるだろう。この3つの戦略はあくまでも例として提供したものだ。
これらのアプローチには、それぞれ独自のコストとメリットがある。最終的に完全な境界に至るまでの代理として、特定の状況においては適切なものである。境界がうまく設定できなければ、劣化していく可能性もある。
アーキテクチャの境界をいつどこに作るのか、それは完全な境界なのか部分的な境界なのかを決めるのは、アーキテクトの役割である。
第25章 レイヤーと境界
■まとめ
ここまでにやってきたことは何を意味するのだろうか?200行のKornshellで実装できるシンプルなプログラムに、どうしてわざわざアーキテクチャの境界を作ったのだろうか?
この例は、アーキテクチャの境界があらゆるところに存在することを示している。我々アーキテクトは、それがいつ必要になるかに気を配らなければいけない。また、境界を完全に構築しようとすると、コストが高くつくことを認識する必要がある。それと同時に、境界を無視すると、たとえ完全なテストスイートやリファクタリングの規律があったとしても、レイヤーを追加するコストが非常に高くなることも認識する必要がある。
では、我々アーキテクトは何をすべきだろうか?その答えは満足できるものではない。非常に頭のいい人たちが、抽象化が必要になることを予測してはいけないと提唱してきた。これがYAGNIの哲学である。このメッセージには、オーバーエンジニアリングのほうがアンダーエンジニアリングよりも悪質であるという知見が含まれている。だが、アーキテクチャの境界が必要なところになかったとしたら、境界を追加するコストやリスクは非常に高いものとなるだろう。
おわかりいただけただろうか。ソフトウェアアーキテクトは未来に目を向けなければいけない。頭を使って推測するべきだ。コストを評価し、アーキテクチャの境界がどこにあるのか、完全に実装する必要があるのか、部分的に実装すべきなのか、無視したほうがいいのかを判断する必要がある。
しかもこれは、1回限りの決定ではないプロジェクトの開始時に、実装する境界と無視する境界を決めればいいわけではない。常に見張る必要がある。システムの進化に注意を払うべきだ。境界が必要になりそうなところに注目して、境界がないために発生している摩擦の兆候を感じ取ってほしい。
そこから、境界を実装するコストと無視するコストを比較検討し、その決定を何度も評価する。無視するコストよりも実装するコストが低くなる変曲点で、境界を実装することがゴールである。
そのためには、注意深く見守らなければいけない。
第26章 メインコンポーネント
■まとめ
Mainをアプリケーションのプラグインと考えよう。初期状態や構成を設定して、外部リソースを集め、アプリケーションの上位レベルの方針に制御を渡すプラグインである。プラグインなので、アプリケーションの設定ごとに複数のMainコンポーネントを持つこともできる。
たとえば、開発用、テスト用、本番用のMainを用意することもできる。あるいは、デプロイする国別、権限別、顧客別に用意することもできるだろう。
Mainをアーキテクチャの境界の背後にあるプラグインとして考えると、設定の問題はもっと解決しやすくなるはずだ。
第27章 サービス:あらゆる存在
■まとめ
サービスは、システムのスケーラビリティや開発の利便性に対しては有用だが、アーキテクチャにおいては重要な要素ではない。システムのアーキテクチャは、システム内の境界と、境界を越える依存性によって定義される。アーキテクチャは、要素の通信や実行の物理的な仕組みで定義されるわけではない。
サービスは、アーキテクチャの境界に囲まれたひとつのコンポーネントの場合もある。あるいは、アーキテクチャの境界で分割された、複数のコンポーネントで構成されている可能性もある。めったにないが、アーキテクチャに重要性を持たせないために、クライアントとサービスを結合させることもあるだろう。
第28章 テスト境界
■まとめ
テストはシステムの外側ではない。安定度やリグレッションのメリットを提供するのであれば、テストもうまく設計すべきシステムの一部である。システムの一部として設計されていないテストは、脆弱で保守が難しくなる傾向がある。保守が難しいので、メンテナンスルームに廃棄されることもよくある。
第29章 クリーン組込みアーキテクチャ
■まとめ
組込みソフトウェアを開発している人は、組込みソフトウェア以外の経験から多くを学ぶことができる。本書を手にしたあなたが組込み開発者であれば、ソフトウェア開発に関する豊富な知見を発見できるだろう。
すべてのコードをファームウェアにすると、プロダクトの長期的な健康のためにならない。ターゲットハードウェアでしかテストできないと、プロダクトの長期的な健康のためにならない。クリーン組込みアーキテクチャは、プロダクトの長期的な健康のためである。
第Ⅵ部 詳細
第30章 データベースは詳細
■まとめ
データの構造であるデータモデルは、アーキテクチャ的には重要である。だが、回転する磁気面上でデータを移動させる技術やシステムは、アーキテクチャ的には重要ではない。リレーショナルデータベースシステムは、データを表形式で管理して、SQLからアクセスするための仕組みであり、これは前者(データモデル)よりも後者(技術やシステム)に近い。データこそが重要なのであり、データベースは詳細なのである。
第31章 ウェブは詳細
■まとめ
このような抽象化は簡単ではないし、適切な抽象化のためには試行錯誤が必要になるだろう。だが、不可能ではない。この世界には怪しげなマーケティングの奴らが満ちあふれている。抽象化が必要になる場面はいくらでも見つかるだろう。
第32章 フレームワークは詳細
■まとめ
フレームワークに出会ったら、すぐに結婚しようとしてはいけない。その前に何らかの方法で時間稼ぎができないかを考えよう。フレームワークは、アーキテクチャの境界から可能な限り遠ざけるようにしよう。牛一頭を購入する以外にもミルクを得る手段はあるはずだ。
第33章 事例:動画販売サイト
■まとめ
図33-2のアーキテクチャ図には、2つの観点での分割が含まれている。単一責任の原則(SRP)にもとづくアクターによる分割と、依存性のルールによる分割である。どちらの分割も、変更の理由や頻度が異なるものを分離することが目的だ。変更の理由に対応するのはアクターによる分割で、変更の頻度に対応するのは方針のレベルの違いである。
コードの構成をこのようにしておけば、システムをデプロイするときにも思いどおりに進められるようになる。デプロイ可能な単位でコンポーネントをまとめることができるし、仮に状況が変わったとしても、コンポーネントのまとめ方を変更しやすい。
第34章 書き残したこと
■まとめ : 言い残したこと
いくらうまい設計をしても、その実装方法の複雑さを考慮しなければ、あっという間に設計が崩れてしまう。これが本章のポイントだ。あなたが望む設計をコードの構造にマッピングする方法、そのコードをとりまとめる方法、実行時とコンパイル時に依存性を分割する方法について考えてみよう。使える選択肢は可能な限り残しておきたいが、理想に走りすぎてもい。チームの規模やメンバーのスキルやソリューションの複雑さ、そして時間と予算の制約などを考慮しよう。選んだアーキテクチャスタイルを守らせるためにコンパイラが使えるかどうかを検討して、データモデルなどのほかの領域と結合してしまわないように注意しよう。悪魔は実装の詳細に宿るものだ。