所感
ソフトウェアを「ソフト」に、柔軟性を保つためのアーキテクチャの考え方が書かれた本です。
いろんなところでも言われているように、ボブおじさんの元ネタ記事以上のことは書かれていません。
しかし、SOLID原則やコンポーネントの安定度などの解説があってからクリーンアーキテクチャの具体的な解説に入るので、元ネタの記事よりもすんなりと受け入れることができました。(ところどころの章にあるソフトウェア昔話も面白かったです。)
表紙にある「アーキテクチャのルールはどれも同じである」の『ルール』とは、「適切に境界線を引いて、方針が詳細に依存しないように作る」ということだと解釈しました。
オープン・クローズドの原則やコンポーネントの安定度を考慮して適切に境界線を引き、依存関係逆転の法則、リスコフの置換原則を適用して方針が詳細に依存しないようインターフェイスを設けることでソフトウェアをソフトな状態に保てます。
ただ、私自身Javaを書いた経験がないため、コンポーネントによるパッケージングといった話はいまいちピンと来ませんでした…。
また、フロント周り(ViewがViewModelを見つける方法など)への言及が少なめのため、実際どうやってJSフレームワークを使ってSPAを構築していくかは頭に浮かびづらいと感じました。この辺は実際にやっている人もいるので、別途参考にしていこうと思います。
まとめ
第Ⅰ部
- ソフトウェアアーキテクチャの目的は、システム構築・運用に必要な人材コストを最小限に抑えるためである。
- システムを急いでリリースすると、だんだんとコード一行あたりのコストも膨れ上がっていく。
- 「急いでリリースして、あとでクリーンにする」ことなど不可能。
- 実は短期的にも長期的にも、崩壊したコードを書く方が、クリーンなコードを書くより遅い。
- TDDで開発したほうが10%早いというデータがある
- ソフトウェアには「ソフト =アーキテクチャ」と「ウェア =機能」という2つの価値がある。
- どっちかが大事じゃなくて、どっちも大事。
- アーキテクチャを後回しにしていると、変更困難なシステムになってしまう。
第Ⅱ部
- オブジェクト指向プログラミング(OO)
- OO最大のメリットはポリモーフィズムである。
- これまでは、制御の流れと依存の流れは一致していた。
- インターフェイス経由で関数を呼び出せば、制御と依存の流れを反転することができる。
- つまり、DBやUIをビジネスルールに依存させることができる。
- すなわち、OOとは「ポリモーフィズムを使用することで、システムにあるすべてのソースコードの依存関係を絶対的に制御する能力」である。
- OO最大のメリットはポリモーフィズムである。
第Ⅲ部 設計の原則
SOLID原則
- SOLID原則とは、関数やデータをどうクラスに組み込み、クラス同士をどう接続させるかの指針になる。
- SOLID原則は、以下の3つの特徴を持ったソフトウェア構造を作るという目的がある。
- 変更に強いこと
- 理解しやすいこと
- コンポーネントの基盤として、多くのソフトウェアシステムで利用できること
SRP:単一責任の原則
- 「どのモジュールもたったひとつのことをやるべき」という原則ではない
- 正しくは、「モジュールを変更する理由はたった一つであるべき」
- 変更理由は、ユーザーやアクターとも読み替えられる
- つまり、「モジュールはたった一つのアクターに対して責務を負うべき」といえる
- データを関数から切り離すことで複数のアクターから責務を負っている問題を解決できる。
OCP:オープン・クローズドの原則
- 上位レベルのコンポーネントは下位レベルのコンポーネントが変更されても、変更する必要はない。
- コンポーネントAがコンポーネントBの変更から保護されるには、コンポーネントBをコンポーネントAに依存させる必要がある。
LSP:リスコフの置換原則
- 抽象型に依存しているコンポーネントは、抽象の派生型に依存しないこと。
- 依存している抽象の、どの派生型でも動く。
ISP:インターフェイス分離の原則
-
使用しない操作はインターフェイスに分離して、依存している箇所をなるべく減らすようにする。
-
必要ない依存を減らすことで、推移的な依存関係もなくすことができる。
-
システムS→フレームワークF→データベースD
というアーキテクチャの場合、SはDにも推移的に依存している。
-
DIP:依存関係逆転の法則
- 「抽象だけに依存しているソースコードが、もっとも柔軟なシステムである」という考え
- 現実的にはすべてを抽象だけに依存するのは難しいので、「変化しやすい具象」にだけは依存しないように設計する。
- 変化しやすい具象とは、開発中のモジュールや、頻繁に変更されるモジュールのこと
- コーディングプラクティスのまとめ
- 変化しやすい具象クラスを参照しない。代わりにインターフェイスを参照する。
- 変化しやすい具象クラスを継承しない。
- 具象関数をオーバーライドしない。
- 変化しやすい具象を名指しで参照しない。
第Ⅳ部 コンポーネントの原則
- どのクラスをどのコンポーネントに含めるか判断するための原則3つ
- 再利用・リリース等価の原則(REP)
- 閉鎖性共通の原則(CCP)
- 全再利用の原則(CRP)
- REP・CCP(コンポーネントを大きくする)とCRP(コンポーネントを小さくする)は相反している。
- どれを優先するかはプロジェクトの状況によって考えるべき。
- 例えばプロジェクト初期ならREP(再利用性)よりもCCP(開発しやすさ)の方が重要視されやすい。
再利用・リリース等価の原則(REP)
- 再利用の単位とリリースの単位は等価になる。
- コンポーネントを再利用したくても、リリース番号がなければうまく再利用できない。
- ひとつのコンポーネントを形成するクラスやモジュールは、同じバージョン・同じリリースプロセス・同じリリースドキュメントを持っていることが合理的である。
閉鎖性共通の原則(CCP)
- 同じ理由、同じタイミングで変更されるクラスをコンポーネントにまとめること。変更の理由やタイミングが異なるクラスは、別のコンポーネントに分けること。
- 同じタイミングで変更されることが多いクラスは、ひとつのコンポーネントにまとめておくこと。
- 変更を1つのコンポーネント内に閉じ込める -> 閉鎖性(OCPの『クローズド』も同じ意味。)
全再利用の原則(CRP)
- コンポーネントのユーザーに対して、実際には使わないものへの依存を強要してはいけない。
- 一緒に用いられることが多いクラスやモジュールは同じコンポーネントにまとめよ、というもの。
- 密結合していないクラスは同じコンポーネントにまとめない。まとめるクラスはどれも切り離せないものばかりにする。
- コンポーネントの関連を扱う原則3つ
- 非循環依存関係の原則(ADP)
- 安定依存の原則(SDP)
- 安定度・抽象度等価の原則(SAP)
非循環依存関係の原則(ADP)
- コンポーネントの依存グラフに循環依存があってはいけない。
- 依存性逆転の原則を用いて循環依存を解消することができる。
安定依存の原則(SDP)
- 安定度の高い方向に依存すること。
- 安定度とは、コンポーネントの「変更しづらさ」のこと。以下のように計算できる。
- ファン・イン…コンポーネントが依存されている外部コンポーネントの数
- ファン・アウト…コンポーネントが依存している外部コンポーネントの数
-
I
(Instability 不安定さ) =ファン・アウト
/ (ファン・イン
+ファン・アウト
)- 0 <=
I
<= 1
- 0 <=
- SDPは、コンポーネントの
I
を依存するコンポーネントのI
よりも大きくすべき(不安定にすべき)である。 - インターフェイスしかないコンポーネントは安定度が高いので、依存するのにちょうどよい。
安定度・抽象度等価の原則(SAP)
- コンポーネントの抽象度は、その安定度の同程度でなければいけない。
- つまり、抽象度が高くなる方向に依存すべき。
- 抽象度は以下のように計算できる。
- Nc…コンポーネント内のクラス総数。
- Na…コンポーネント内の抽象クラスとインターフェイス総数。
-
A
(抽象度)=Na
/Nc
- 0 <=
A
<= 1
- 0 <=
- 抽象度が低く、安定度が最高のコンポーネント(A=0, I=0)は、柔軟性にかけるので好ましくない。苦痛ゾーンと呼ばれている。
- 変動性の低いコンポーネントは変更されることがないため、苦痛ゾーンにあっても問題ない。
- 抽象度が高く、安定度が低いコンポーネント(A=1, I=1)は、抽象化されているのに依存するコンポーネントが存在しない、無駄ゾーンである。
- コンポーネントにとっての理想的な場所は、抽象度が低く、安定度が低い場所(A=0, I=1)もしくは抽象度が高く、安定度が高い場所(A=1, I=0)である。
- この2点を結ぶ直線を主系列と呼び、なるべくこの線に乗せるようにする。
第Ⅴ部 アーキテクチャ
- 優れたアーキテクチャは以下のことをサポートする必要がある。
- システムのユースケース
- 振る舞いを明らかにして、システムの意図を明確にすること。
- 優れたアーキテクチャのショッピングカートシステムは、ショッピングカートに見える。
- システムの運用
- 顧客の増加に従ってシステムをスケールアップできること。
- コンポーネント間通信を特定の技術基盤に依らない設計にすれば、簡単にスレッド→プロセス→サービスと移行できる。
- システムの開発
- チーム間で干渉しないように、コンポーネントを単独で開発できるようにする。
- システムのデプロイ
- 構成スクリプトや設定ファイルの変更に依存せずに「即時デプロイ」を目指す。
- システムのユースケース
- ビジネスルールとそれ以外の間に境界線を引くことで、ビジネスロジック以外のアーキテクチャ決定を遅延することができる。
レベル
- 下位レベルのコンポーネントが上位レベルのコンポーネントに依存すべきである。
- 入出力が近ければ近いほど、下位レベルに位置づけられる。
エンティティ
ユースケース
- アプリケーション固有のビジネスルール(バリデーション等)、エンティティを呼び出す順番を記したオブジェクトを「ユースケース」と呼ぶ。
- 特定のユーザーインターフェースについては規定せず、渡されるデータ形式を規定する。
- すなわち、ユースケースの入出力のデータは
HttpRequest
やHttpResponse
等にはならない。 - 入出力のデータにエンティティを使わない。「入出力データ形式」と「エンティティ」は変更する理由が異なるため。
- すなわち、ユースケースの入出力のデータは
クリーンアーキテクチャ
- クリーンアーキテクチャを採用したシステムの特徴
- フレームワーク非依存
- テスト可能
- UI, データベース, Webサーバ等がなくてもビジネスルールをテストできる。
- UI非依存
- データベース非依存
- 外部エージェント非依存
- ビジネスルールは外界のインターフェイス(低レベルのコンポーネント)について何も知らない。
- 依存性のルール
- ソースコードの依存性は高レベルのコンポーネントにのみ向かってなければいけない。
- 図で言うところの「外側」から「内側」にのみ依存してよい。
- ソースコードの依存性は高レベルのコンポーネントにのみ向かってなければいけない。
- 各レイヤの責務
- エンティティ
- 外部で何が起きても変更されることがない。
- ユースケース
- UIやDBの変更がこのレイヤに影響を及ぼすことはない。
- アプリケーションの操作の変更のみによって影響される。
- インターフェイスアダプタ(図の
Controller
,Presenter
,Gateways
に該当する部分)- ユースケース・エンティティ ⇔ DB・Web等の外部エージェント間で、それぞれの利用しやすいデータ構造を変換する層。
- ユースケース層とインターフェイスアダプタ層間は、単なるデータ構造である「モデル」でデータの受け渡しをする。
- ユースケース・エンティティ ⇔ DB・Web等の外部エージェント間で、それぞれの利用しやすいデータ構造を変換する層。
- フレームワークとドライバ
- フレームワークやデータベースと直接やり取りするコンポーネント。
- エンティティ
- 境界線の越え方
- 高レベルのコンポーネントから低レベルのコンポーネントへ通信するときは、依存関係逆転の原則を使って境界を超える。
- 境界線を超えるデータは単なるデータ構造であるべき。エンティティや、データベースの行をそのまま渡すのは依存性のルールに違反する。