Clean Architecture
イントロダクション
アーキテクチャの目的
要求されたシステムを構築・保守するのに必要な人材を最低限に抑えること。 設計の品質は、顧客のニーズを満たすために必要な労力で測定できる。
ソフトウェアの提供価値
ステークホルダーに「振る舞い」と「構造」を提供する。 開発者のジレンマは、ビジネスマネージャーがアーキテクチャの重要性を評価できないことにある。
急がば回れ
クリーンなコードより、崩壊したコードを書くほうが常に遅い。 開発者たちは「あとでクリーンにすればいいよ。先に市場に出さなければ!」と言っていつもごまかす。
プログラミングパラダイム
半世紀で我々が学んだのは、何をすべきではないかである。 パラダイムはプログラマの能力を制限してきた。 現在のソフトウェアのルールは1946年と同じ。ソフトウェアの本質は変わっていない。
構造化プログラミング(1968年)
直接的な制御の移行に規律を課す(goto文の制限)
Dijkstraは、goto文によって「分割統治」が使えなくなることを発見した。 BohmとJacopiniは、あらゆるプログラムは3つの構造「順次(順番に実行)」「分岐(if文)」「反復(for文)」で構築できると特定した。
オブジェクト指向プログラミング(1966年)
間接的な制御の移行に規律を課す(関数ポインタの制限)
オブジェクト指向とは「ポリモーフィズムによるソースコードの依存関係の絶対的制御」である。
ポリモーフィズム
同じ名前のメソッドが異なるクラスで異なる振る舞いをするという概念で、関数へのポイントの応用である。
依存関係逆転
\(ML1\)とインターフェイス\(I\)のソースコードの依存関係が、制御の流れと逆転している。 オブジェクト指向言語がポリモーフィズムを提供しているということは、ソースコードの依存関係は逆転できることを意味する。
アーキテクチャへの応用
UIとデータベースをビジネスルールのプラグインにでき、UIやデータベースの変更からビジネスルールを保護できる。
関数型プログラミング(1936年)
代入に規律を課す(代入の制限)
可変変数の問題
可変変数は、競合状態、デッドロック状態、並行更新等の問題を引き起こす。
可変性の分離
不変コンポーネントの処理を多くし、可変コンポーネントの処理を少なくするべき。
イベントソーシング
状態ではなく取引(トランザクション)を保存する戦略のこと。 すべての取引を集計することで状態を得られる。 十分な記憶容量と処理能力があれば、アプリケーションは完全に不変(完全に関数型)にできる。
設計の原則
SOLID原則は、以下のようなクラス設計の指針を教えてくれる。
- 関数やデータ構造をどのクラスに組み込むか
- クラスの接続をどうするか
単一責任の原則(SRP)
Single Responsibility Principle
モジュールはたった1つのアクターに対して責務を負うべき。
アクターとは「変更を望む人たち」のことで、モジュールとは「関数やデータをまとめたもの」のこと。 モジュールを変更する理由がたった1つだけになるように、組織の社会的構造をシステムの構造に反映する。
症例1:想定外の振る舞い
別々のアクターの要望で様々な仕様が混在し、想定外の振る舞いとなる。
症例2:コンフリクト
別々のアクターの要望で同一ソースファイルを変更し、コンフリクトが発生する。
オープン・クローズドの原則(OCP)
Open-Closed Principle
ソフトウェアの振る舞いは、既存の成果物を変更せずに拡張できるようにすべき。
アーキテクトは、いつどのような理由で、どのように変更するかを考えて機能を分割する。 分割した機能をコンポーネントの階層構造にまとめる。
リスコフの置換原則(LSP)
Liskov Substitution Principle
システムのパーツを交換可能にするには、共通の契約に従わなければいけない。
インターフェイスを実装することで交換可能になる。 そうしないと特別な仕組み(交換不可な実装)だらけになる。
インターフェイス分離の原則(ISP)
Interface Segregation Principle
不使用な依存は回避すべき。
不要なモジュールへの依存は、再コンパイル・デプロイを強制されるため有害である。
依存関係逆転の原則(DIP)
Dependency Inversion Principle
下位レベルの詳細は、上位レベルの方針に依存すべき。
インターフェイスは実装よりも変化しにくい。インターフェイスの変動性を抑える。 新機能追加時は、できる限りインターフェイスの変動なしで済ませる。
- 具像クラスを参照・継承しない
- 具像関数をオーバライドしない(元の関数を抽象関数にする)
具像コンポーネント
この原則に完全に違反しないことはできない。違反するコンポーネントは分離する。
レベル
レベルは、「入力と出力からの距離」と定義される。 ソースコードの依存性は、データフローから切り離し、下位レベルが上位レベルのコンポーネントに依存するように設計する。 依存された上位レベルのコンポーネントは、下位レベルのコンポーネントの変更から保護され、変更する必要がない。 最上位レベルのコンポーネントは、方針であるビジネスルールを含み、特権的な位置づけになる。
- 上位レベルの方針 … 変更の頻度が低く、変更の理由が重要である
- 下位レベルの方針 … 変更の頻度が高く、変更の理由は重要ではない
コンポーネントの原則
コンポーネントとは「デプロイの単位」のこと。 優れたコンポーネントは、常に個別にデプロイできる状態を保ち、個別に開発を進められる。 コンポーネントの構造は、要件の変化に伴って「変化」する。
コンポーネントの凝集性
テンション図
開発初期は、開発しやすさが優先され、右側に位置する。 プロジェクトが進み、別のプロジェクトから再利用される頃になると、左側へ移っていく。 コンポーネントの構造は、経過時間や成熟度によって変化する。
再利用・リリース等価の原則(REP)
再利用の単位とリリースの単位は等価になる。
一貫したテーマや目的を共有するモジュールを集める。 リリースプロセスにおいては、適切な通知とリリースドキュメントが欠かせない。
閉鎖性共通の原則(CCP)
同一理由、同一タイミングで変更されるクラスを同一コンポーネントにまとめる。
「単一責任の原則」を言い換え。 多くの場合、再利用性よりも保守性のほうが重要。
全再利用の原則(CRP)
不要な依存を強制しない。
「インターフェイス分離の原則」の一般化。依存するなら含まれるすべてのクラスに依存する。
コンポーネント図と有向グラフ
コンポーネント図は、ビルド可能性や保守性を見るための地図で、機能について書くことはほぼない。
変動性の分離
依存構造のなかで最優先に対処すべき問題が、変動性の分離である。 頻繁に変更されるコンポーネントが、安定すべきコンポーネントに影響を及ぼすことを避ける。 コンポーネントの依存構造は、システムの理論設計に合わせて育てていく。
非循環依存関係の原則(ADP)
コンポーネントの依存グラフに循環依存があってはいけない。
循環したコンポーネントは、事実上ひとつの巨大なコンポーネントになり、切り離すのが難しくなる。 ユニットテストやリリースも難しくなり、失敗に陥りやすくなる。
循環依存の解消
有向非循環グラフ(どのコンポーネントから矢印をたどっても元に戻ってこない)にする。
- 依存関係逆転の原則を適用
- 両方が依存する新しいコンポーネントを作成
リリースの影響範囲
コンポーネントのリリースで影響を受けるのは、そのコンポーネントに依存しているコンポーネントのみ。 新しいリリースをいつ導入するかは、各チームで判断できる。
コンポーネントの安定度と抽象度
安定依存の原則(SDP)
安定度の高い方向に依存する。
- 独立コンポーネント … 外部要因で変更が必要にならない(安定したコンポーネント)
- 従属コンポーネント … 外部要因で変更が必要になる(不安定なコンポーネント)
安定度の指標 \(Instability\)
安定依存の原則は、コンポーネントの依存性を順番に辿ると、Instabilityは減少していくべきという原則。
\[I = \frac{FO}{FI + FO}\]記号 | 名称 | 説明 |
---|---|---|
I | Instability | 不安定さ。0ほど安定、1ほど不安定。依存しているほど不安定。 |
FO | ファン・アウト | 依存出力数。依存している数。 |
FI | ファン・イン | 依存入力数。依存されてる数。 |
安定度・抽象度等価の原則(SAP)
安定度の高いコンポーネントは抽象度も高くあるべき。
安定度の高さが拡張の妨げになってはいけない。 抽象度が高くなる方向に依存すべき。
抽象度の指標 \(Abstraction\)
\[A = \frac{Na}{Nc}\]記号 | 名称 | 説明 |
---|---|---|
A | Abstraction | 抽象度 |
Na | Number of Abstract class | コンポーネント内の抽象クラスとインターフェイスの総数 |
Nc | Number of Class | コンポーネント内のクラスの総数 |
安定度と抽象度
主系列の両端のどちらかが、コンポーネントには理想的な場所。
主系列からの距離 \(Distance\)
\[D = |A + I - 1|\]0ほど主系列に近く、1ほど遠い。 距離の離れたコンポーネントは、精査する価値がある。距離の推移を記録し、傾向を監視することも有効。
苦痛ゾーン
柔軟性に欠けている。ただし、問題になるのは変動性の高いコンポーネントだけ。 オブジェクト指向アプリケーションとデータベースのインターフェイスはここになる。 きちんと管理するのは難しく、スキーマの変更は苦痛をともなう。
無駄ゾーン
実装のない抽象クラス。
アーキテクチャ
ソフトウェアアーキテクトはプログラマである。 自分で課題を経験していなければ、ほかのプログラマのために適切な仕事をすることなどできない。
ソフトウェアの要素
- 方針 … ビジネスのすべてのルールや手順を含んでいて、システムの本当の価値がある。
- 詳細 … 方針の振る舞いに影響を与えるものではない。
アーキテクチャとは?
システムの形状である。コンポーネントの分割・配置・通信によって、形をなす。 アーキテクチャは、システムの動作に影響を与えるものではない。
アーキテクチャの目的
ライフサイクル(開発・デプロイ・保守)を容易にすること。ライフサイクルコストを最小限に抑え、プログラマの生産性を最大にする。
ライフサイクル
アーキテクチャを慎重に考え抜けば、ライフサイクルコストを大幅に低下させられる。
デプロイ
単一のアクションで簡単・即時にシステムをデプロイできるようにする。
保守
保守は最もコストがかかる。
保守の主なコストは、洞窟探検とリスクだ。
洞窟探検とは、新しい機能の追加や欠陥の修正において、既存のソフトウェアを掘り起こし、最適な場所や戦略を見つけること。
システムをコンポーネントに分離し、安定したインターフェイスを持つ独立したコンポーネントにしておけば、将来の機能の道筋が照らされ、意図せずに壊してしまうリスクを大幅に軽減できる。
運用
運用の問題の多くは、ハードウェアの追加で解決できる。 ハードウェアは安価で、人は高価である。 デプロイ・開発・保守を優先するアーキテクチャのほうがコストは安い。
アーキテクトの戦略
できるだけ長い期間、できるだけ多くの選択肢を残すこと。
周囲の詳細を気にせず、上位の方針を構築できれば、詳細の決定をしばらく延期・保留できる。 決定を延期できれば、その分だけ適切に作るための情報が数多く手に入る。
独立性
システムの切り離し方式は時間とともに変化する。変化を予見して、適切に進める。
独立性のレベル
システムはモノリシックとして誕生し、独立してデプロイ可能な単位になるまで成長し、最後は単体のサービスにたどり着く。
- ソースレベル
- デプロイレベル
- サービスレベル
コンウェイの法則
システムの設計には、組織のコミュニケーション構造が反映される。
ユースケース
アーキテクチャはユースケースをサポートしなければいけない。 システムの構造にハッキリとユースケースが表れたアーキテクチャは優れている。
ユースケースに基づいた切り離し
システムを水平レイヤーに分割するとき、それらを薄く垂直にユースケースとしても分割する。 ユースケースはシステムを分割する自然な方法である。 ユースケースの切り離しは、運用にも適用可能である。
切り離しにともなう重複
切り離しで重複は発生し得る。重複を安直に排除してはいけない。重複が本物かどうかを見極めるべき。
独立性とスケーラビリティ
開発とデプロイの独立性は、スケーラブルであると見なされる。 サービスは、スケーラビリティや開発の利便性に対しては有用だが、アーキテクチャには重要な要素ではない。
- 独立デプロイ可能性
- コンポーネントのソースコードを変更しても、そのコンポーネントだけを再デプロイすればいい。
- 独立開発可能性
- システムにあるモジュールを個別にデプロイできるなら、別々のチームが個別に開発できる。
バウンダリー
アーキテクチャとは、境界線を引く技芸である。境界線は、方針と詳細を明確にする。方針に専念し、詳細の決定を引き伸ばす。 我々はシステムの振る舞いをIOの振る舞いから考えてしまう。インターフェイスはモデル(ビジネスルール)にとって重要ではない。
境界線を引く
- システムをコンポーネントに分割する
- コンポーネントを整理する
- [方針]コアコンポーネント(ビジネスルール)
- [詳細]プラグイン(ビジネスルールを含まない)
- 依存性を整理する
- [方針]コアコンポーネントに向かって矢印を描く
ソフトウェア開発技術の歴史は、いかに都合よくプラグインを作成するかの物語である。
publicによる崩壊
publicはパッケージを消滅させる。 具像実装クラスを直接インスタンス化するコードを防ぐ手段がなくなる。
詳細の引き伸ばし
優れたアーキテクチャは、重大な影響を与えることなく、詳細の決定を引き延ばせる。
成功事例
開発初期に、ビジネスルールとデータベースの間に境界線を引いた。 それにより、データベースの選択を1年以上も遅らせられた。 様々な選択肢を試し、より良いソリューションを選択できた。
失敗事例
3つのサービスに分割すると早々に決定し、実行ファイルを3つに分けた。 しかし、結局1台のサーバでまとめてサービスを動かすことになった。 アーキテクチャの早すぎる決定で、開発の労力が劇的に増えてしまった。
オーバエンジニアリングは悪
境界の完全な構築はコストが高い。 境界をどのレベル(完全・部分的・無視)で実装するか、システムの進化に合わせて適宜検討する。 オーバーエンジニアリングは、アンダーエンジニアリングよりも悪質である。
ビジネスルール
ビジネスルールはシステムのなかで、最も独立していて、最も再利用可能なコードでなければいけない。
最重要ビジネスルール
ビジネスに不可欠で、システムとは無関係に存在するルールのこと。
最重要ビジネスデータ
システムとは無関係に存在するデータのこと。
エンティティ
エンティティとは、最重要ビジネスデータを操作する最重要ビジネスルールを含んだオブジェクトのこと。 エンティティはビジネスであり、ビジネスに不可欠な概念をクラスにまとめ、独立させる。 エンティティは詳細を何も気にする必要はない。
ユースケース
ユースケースとは、システムを使用する方法を記述したものであり、エンティティをいつ・どのように呼び出すかを規定したルールからなる。 ユースケースはインターフェイスを知らず、エンティティはユースケースを知らない。
リクエストとレスポンス
ユースケースクラスは、入力と出力にシンプルなデータ構造を使用する。 目的が異なるため、データ構造にエンティティオブジェクトを参照してはいけない。
ユースケースを叫ぶアーキテクチャ
戸建てや図書館の設計図が建物のユースケースを叫んでいるように、 ソフトウェア・アプリケーションのアーキテクチャも、アプリケーションのユースケースを叫ぶべき。 優れたアーキテクチャは、ユースケースを中心にしている。 提供方法(ウェブ等)やフレームワークに影響を受けるべきではない。
クリーンアーキテクチャ
コンポーネント図サンプル
レイヤー
インターフェイスアダプター
円の内側(ユースケース)と円の外側(ウェズ、DB)のデータフォーマットを変換する。
フレームワークとドライバ
このレイヤーにはコードをあまり書かない。詳細が詰まっている。 Viewがやるべきことは、ViewModelからデータを読み込み、画面に表示すること以外に残されていない。
ルール
依存性
内側だけに依存しなければいけない。内側は外側について何も知らない。
境界線を超えるデータ
内側にとって便利な形式であり、単純なデータ構造で渡す。 エンティティオブジェクトやデータベースの行をそのまま渡すことはしない。
踏襲されたアーキテクチャ
「ヘクサゴナルアーキテクチャ」、「DCIアーキテクチャ」、「BCE」の3つ。
共通点:関心事の分離
ソフトウェアを「ビジネスルール」と「インターフェイス」のレイヤーに分割する。 それにより、UI・データベース・フレームワーク等への依存性をなくし、テストを可能にしている。
レイヤードアーキテクチャ
とりあえず動くものを複雑になりすぎないように手早く作るには優れた設計。
- 厳格なレイヤードアーキテクチャ … 隣接する下位レイヤーだけに依存する。
- 緩いレイヤードアーキテクチャ … ユースケースによって、ControllerがServiceを飛び越える。
テスト
テスト容易性が優れたアーキテクチャの特性である。 テストは非常に詳細で具体的であり、テストするコードに対して常に依存している。
Humble Objectパターン
- Humble Object … テストが難しい振る舞いのみが含まれる
- それ以外 … テストしやすい振る舞いのみが含まれる
脆弱なテスト
1つの共通コンポーネントを変更すると、何百何千と壊れてしまうテストのことを脆弱なテストという。 脆弱なテストはシステムを硬直化させる。
詳細
Mainコンポーネントは究極的詳細
システムの最初のエントリーポイントである。 Mainコンポーネントで、DIフレームワーク等を利用した依存関係の注入が行われる。
データベースは詳細
データベースは、ディスクとRAMの間でデータを移動する仕組みにすぎない。
データベースによる汚染
フレームワークの多くは、データベースの行やテーブルをオブジェクトとして受け渡しできるようになっている。 これにより、EntityやViewModelもリレーショナルデータ構造に影響を受けている。 アーキテクチャ的には間違っており、下位レベルの仕組みには汚染されることを許してはいけない。
フレームワークは詳細
フレームワークを使うと決めた時点で、フレームワークに縛られることになると認識しておく。
動画販売サイトの設計事例
ユースケース分析
4つのアクターそれぞれが、システム変更の要因になる。 あるアクターに関する変更が、他のアクターへ影響を及ぼさないようにシステムを分割する。
コンポーネントアーキテクチャ
制御は右から左へ流れる。コントローラーから受け取った入力をインタラクターが処理する。 プレゼンターが結果をフォーマットして、ビューがそれを表示する。
アーキテクチャ分割の観点
- アクターによる分割(単一責任の原則)
- 依存性のルールによる分割