2.2 ソフトウェア設計原則
概要と動機
ソフトウェア設計原則とは、時間をかけてコードを理解し、変更し、拡張できるようにコードを配置するためのヒューリスティクスです。名前付きの頭字語(SOLIDは五つのオブジェクト指向設計原則、DRYは繰り返すな、KISSは単純にしておけ、YAGNIはそれは必要にならない)、構造的な概念(結合度、凝集度、関心の分離)、体系化されたデザインパターン、ドメイン駆動設計(ビジネス領域の言葉でソフトウェアをモデル化する)のようなより高水準のモデル化アプローチ、そしてオブジェクト指向、関数型、データ指向のスタイルの選択を含みます。これらはどれも法則ではありません。圧縮された経験であり、判断をもって適用しなければなりません。
大きなチームにとって、共有された原則の価値は調整です。何百人ものエンジニアが同じシステムに取り組むとき、設計の議論のための共通の語彙と、独立して書かれたモジュールが組み合わさるための共通の既定のセットが必要です。良い設計とは、多くの人が絶え間ない衝突なしに、並行してシステムを変更できるようにするものです。また、企業や政府のシステムの通常の寿命である十年後にも、元の作者の在任期間をはるかに過ぎてもシステムが変更可能であり続けるようにするものでもあります。
重要なスキルは、原則を暗記することではありません。それぞれがいつ誤導するかを知ることです。すべての原則には失敗モードがあります。DRYは誤った抽象化を生みえ、SOLIDは不要な間接層を生みえ、YAGNIは本当に必要な拡張性を飢えさせえます。本章は原則を、適用範囲を持つ道具として扱い、頭字語が仕えようとしているより深い特性として、結合度と凝集度を強調します。
主要原則
- まず結合度と凝集度を管理します。名前付きの原則の大半は、この二つの特性を改善する間接的な方法です。
- 変更のために最適化します。良い設計は、実際に行う必要のある変更のコストを最小化します。
- 今動く最も単純な設計を好みますが、変更がありそうな所には境界を保ちます。
- 重複は、誤った抽象化より安上がりです。パターンが明確になるまで待ちます。
- 依存関係を明示的にし、安定したものに向けます。
- ドメインをドメインの言葉でモデル化し、ソフトウェアの境界をビジネスの境界に合わせます。
- パラダイムは、イデオロギーではなく問題に合わせて選びます。大きなシステムの大半は、実用的に混在しています。
推奨事項
SOLIDをチェックリストではなくレンズとして使う
単一責任はモジュールを凝集させるために、依存性逆転は本当に境界がある所で依存を抽象に向けるために、開放閉鎖は拡張点が本物である所に適用します。実装が一つしかなく、二つ目が見えない状況で、頭字語を満たすためだけにインターフェース、ファクトリー、レイヤーを作り出してはいけません。間接層にはコストがあり、読むたびに支払うことになります。
DRYをテキストではなく知識に適用する
DRYは、単一の権威ある知識の断片を重複させないことです。単に似ている行を排除することではありません。似て見えても異なる理由で変わる二つのコードは、分けておくべきです。無関係なものを結合する早すぎる共有の抽象化よりも、少しの重複を好みます。本物のパターンが二、三回現れてから、抽象化を取り出します。
KISSとYAGNIで憶測に抵抗する
想像する要件ではなく、持っている要件のために構築します。設定可能なフレームワーク、プラグインシステム、誰も求めていない拡張点のような、憶測による一般性を避けます。対となる均衡は、安定したインターフェースやきれいな継ぎ目のように、早くに組み込んだほうが本当に安い柔軟性もあることです。YAGNIは憶測による実装に反対するのであって、思慮深い境界に反対するのではありません。
低結合と高凝集を明示的に設計する
各モジュールに、明確に定義された一つのことを行わせ(凝集)、狭いインターフェースを通じて、できる限り少数の他のモジュールにだけ依存させます(低結合)。設計をレビューするとき、どの変更がモジュールの境界をまたいで波及するかを問います。それらの波及が、結合度の真の尺度です。関心の分離は、同じ考えをレイヤーと横断的関心事に適用したものです。
デザインパターンを語彙として使い、アンチパターンを警告として適用する
パターンは、繰り返し現れる解決策への有用な共有の名前です。問題が実際にそれに合致するときに手に取ります。洗練されて見せるためにパターンを押しつけてはいけません。パターンだらけのコードは、しばしば過剰設計の兆候だからです。一般的なアンチパターン(ゴッドオブジェクト、不適切な所の貧血モデル、ビッグボールオブマッド、分散モノリス)を、診断のラベルとして学びます。
ドメインが複雑な所でドメイン駆動設計を採用する
豊かなビジネスルールを持つシステムには、DDDの戦術的・戦略的な道具を使います。ドメインエキスパートと共有されるユビキタス言語、システムを独立してモデル化される部分に切り分ける境界付けられたコンテキスト、それらの部分がどう関係するかを記述するコンテキストマップ。境界付けられたコンテキストは、チームのオーナーシップをモデルの境界に揃えるので、企業の規模で特に価値があります。DDDは、単純なCRUD(作成、読み取り、更新、削除)のシステムには過剰です。
パラダイムを適合性で選ぶ
状態を持つ振る舞いのカプセル化とドメインのモデル化にはオブジェクト指向を使います。変換、並行性、不変性による予測可能性には関数型スタイルを使います。性能とキャッシュの振る舞いが支配的な所にはデータ指向設計を使います。大きなシステムは三つすべてを混ぜます。選択をコンポーネントごとに行い、スタイル間の境界をきれいに保ちます。
トレードオフ: 長所と短所
| 原則 / アプローチ | うまく適用された場合 | 失敗モード |
|---|---|---|
| SOLID | 変更が起こる所の明確な継ぎ目。テスト可能な単位 | インターフェースとレイヤーの増殖。見返りのない間接層 |
| DRY | 本物の知識に対する唯一の信頼できる情報源 | 無関係なコードを結合する誤った抽象化 |
| KISS / YAGNI | 無駄がなく理解可能なシステム | 設計不足の継ぎ目。必要な柔軟性を後付けする高いコスト |
| デザインパターン | 共有された語彙。実績のある構造 | パターンの盲信。偶発的な複雑さ |
| ドメイン駆動設計 | 揃ったモデルとチーム。手なずけられた複雑さ | 単純なドメインでの重い儀式。ずれたコンテキストの境界 |
| 関数型 / 不変 | 予測可能性。より安全な並行性 | 本質的に状態を持つ問題には不向き。性能の意外性 |
繰り返し現れる緊張は、設計不足と過剰設計の間にあります。設計不足のシステムは結合を蓄積し、硬直します。過剰設計のシステムは、誰かが理解し保守しなければならない抽象化に溺れます。答えは固定点ではなく、規律です。十分な情報が得られるまで決定を先送りしつつ、考えを変えられる継ぎ目を保ちます。
チームで議論すべき問い
共有の抽象化を取り出すための具体的な閾値は何で、DRYが誤った抽象化を生まないようにするにはどうしますか。 本章は、重複は誤った抽象化より安上がりであり、取り出す前にパターンが二、三回現れるまで待つべきだと率直に述べています。大きなチームでは、危険は、誰かが似た二つのスニペットをチームの境界をまたぐ共有モジュールにまとめ、以後、一方の呼び出し元への変更がすべて他方に波及することです。持ち込むべきシグナルは、重複が同じ理由で変わるのか、それとも今たまたま似ているだけなのかです。三回ルールに合意し、呼び出し元を結合する前に、候補の抽象化が実際に一緒に変更されたことを求めてください。その一つの合意が、多くのチームが依存してしまうと解きほぐすのに高くつく、ある種の結合を防ぎます。
設計レビューで、結合度と凝集度を、直感に任せず可視化するにはどうしますか。 主要原則は、結合度と凝集度をあらゆる頭字語の上に置き、結合をモジュールの境界をまたいで波及する変更として定義しています。直感は、それぞれがシステムの自分の隅しか見えない何百人ものエンジニアにわたってはスケールしません。機械が生成できる証拠を持ち込んでください。依存関係グラフと、どのモジュールが同じコミットで一緒に編集され続けているかを示す共変更データ。変更がどのモジュールの境界をまたがせるかを問う、明示的なレビューの質問を加えます。二つのモジュールが常に一緒に変わるなら、それは、それらをマージするか、間の境界を直すかの合図です。
システムのどこに、ドメイン駆動設計を正当化するほど豊かなドメインと、それが過剰になる単純なCRUDアプリとの境界線がありますか。 本章は、DDDの境界付けられたコンテキストがチームのオーナーシップをモデルの境界に揃えるからこそ推奨し、DDDは単純な作成・読み取り・更新・削除のシステムには過剰で、本物のモデル化なしには儀式に堕ちると警告しています。どちらの方向に誤ってもコストがかかります。薄いドメインへの重いDDDは単純なアプリを儀式に埋もれさせ、多くのチームにわたる広がった共有モデルは、絶え間ないチーム間の調整を強います。実際にそれを決めるシグナルを持ち込んでください。ビジネスルールの密度と、いくつのチームが独立して部分を所有する必要があるか。戦略的な仕掛けは複雑なコアのために取っておき、単純な周辺は単純なままにします。そうすれば、DDDの芝居とビッグボールオブマッドの両方から遠ざかれます。
抽象化、インターフェース、デザインパターンが、それが加える間接層に見合うのはいつで、設計が過剰設計だと言う権限は誰にありますか。 本章は、間接層には読むたびに支払うコストがあり、SOLIDを満たすため、あるいは洗練されて見せるためにインターフェース、ファクトリー、レイヤーを作り出すことは失敗モードだと明言しています。大きなチームでは、圧力は逆方向に働きます。レビュアーは規律があるように見えるので追加の抽象化を通し、誰も構造を減らせと主張する人になりたくないのです。相反する考慮は本物です。一部の継ぎ目は確かにその価値があり、後で取り除くのは高くつくからです。議論に具体的な証拠を持ち込んでください。インターフェースが今日実際にいくつの実装を持つか、拡張点がこれまで何度柔軟に使われたか、一つのコードパスをたどるのに読み手が何ファイルを開く必要があるか。二つ目が見えない一つの実装は、インライン化する既定の理由だと合意し、設計を過剰設計とラベル付けしても、侮辱として読まれないようにする権限を持つのが誰かを名指ししてください。作者より十年以上長生きする企業や政府のシステムでは、理由のない間接層は、将来のすべての保守担当者が払う税なので、「この抽象化は何を買うのか」を、個人的な挑戦ではなく、常設のレビューの質問として扱ってください。
各コンポーネントがどのパラダイム、オブジェクト指向、関数型、データ指向を使うかをどう決め、それらの間の境界をどうきれいに保ちますか。 本章は、大きなシステムは実用的に混在していて、コンポーネントごとに適合性で選ぶべきだと論じています。状態を持つドメインにはオブジェクト指向、変換と並行性には関数型、性能とキャッシュの振る舞いが支配的な所にはデータ指向設計です。管理しなければ、パラダイムの選択は最初にモジュールを書いた人次第になり、可変状態が純粋であるべき変換に漏れ出したり、関数型の純粋主義が本質的に状態を持つ問題と戦ったりします。持ち込む価値のある証拠は、実際の痛みがどこにあるかです。隠れた状態のせいでテストが難しいコンポーネント、キャッシュに律速されるホットパス、現在のスタイルがぎこちない回避策を強いる所。各層の既定のパラダイムを意図して決め、スタイル間の継ぎ目がどこに落ちるかを書き留め、関数型のコアと命令型の周縁が互いに染み出さないようにします。計算が、ある期間について監査可能かつ再現可能でなければならない規制されたシステムや政府のシステムでは、不変の関数型のコアは、好みではなく遵守要件であることが多く、その制約は、境界に従うのではなく、境界を導くべきです。
これらの原則が教条に固まらないようにするにはどうし、将来のチームが見直せるよう、設計の決定の背後にある理由をどこに記録しますか。 本章のあらゆる原則には、適用範囲と失敗モードがあり、枠組み全体が、それらを強制すべき法則ではなく、判断をもって適用する道具として扱っています。大きなチームでは、原則は静かにルールになります。DRYはあらゆる重複を禁じ、SOLIDはクラスごとのインターフェースを義務づけ、実用的な例外は、成果ではなく頭字語を引用する人々によってレビューでブロックされます。緊張は、ある程度の一貫性が、何百人ものエンジニアの調整に確かに役立つので、すべての原則を任意と宣言するわけにはいかないことです。原則を字義通りに守って、より悪い設計になった例と、もしあれば、ある境界や抽象化がなぜ存在するかを説明する決定の記録を持ち込んでください。原則は、記録された理由があればエンジニアが逸脱してよい既定であると合意し、重要な設計の選択を短いアーキテクチャ決定記録に残して、次のチームがコードだけでなく理由も引き継げるようにします。元の作者がとうに去り、監査がなぜシステムがこの形なのかを問う企業や公共部門のシステムでは、その書面の跡が、将来のチームが安全に変更できる設計と、触れるのを恐れる設計の違いです。
セクター別の視点
スタートアップ。 出荷できる最も単純な設計を好み、本物の二つ目のユースケースが継ぎ目を強いるまで、よく因数分解された一つのモジュールを保ってください。最も乏しい資源はエンジニアリングの注意なので、早すぎるインターフェース、レイヤー、憶測によるフレームワークは純粋なコストです。共有の抽象化を取り出す前に三回ルールに従い、YAGNIに、まだ誰も求めていない拡張点を殺させます。
小規模事業者。 専任のアーキテクトはおらず予算も厳しいので、独自のパターンを発明するのではなく、買うフレームワークやライブラリにすでに組み込まれた設計に頼ってください。独自の設計の労力は、本当に自社のビジネスである少数のルールのために取っておき、他はすべて慣例どおりにして、契約者や新規採用者が読めるようにします。理解できる少しの重複は、作者だけが保守できる巧妙な抽象化に勝ります。
大企業。 共有された原則の見返りは、多数のチームにわたる調整です。設計レビューのための共通の語彙と、モデルの境界をチームのオーナーシップに揃えて各グループが独立して進化できるようにする境界付けられたコンテキスト。依存関係と共変更のデータで結合度と凝集度を明示的に管理し、重要な設計の決定を記録して、作者が去ったずっと後もシステムが変更可能であり続けるようにします。チームを結合する誤った抽象化と、すべての読み手に課税する過剰設計の両方を、等しく警戒してください。
政府。 監査可能性と再現可能性が、しばしば設計を決めます。不変の関数型のコアは、ある期間について過去の計算を正確に再現できるようにしますが、隠れた可変状態を持つ絡み合ったオブジェクトグラフには、それが保証できません。コンテキストの境界では、共有テーブルより、明示的に公開された契約を好み、設計とその決定の記録を、監査人と、十年後にシステムを引き継ぐどのチームにも読めるように保ちます。
事例
スタートアップ。 最初の製品を作る3人のスタートアップは、あらゆる機能をインターフェースとファクトリーの層に分割したい衝動に抵抗し、本物の二つ目のユースケースが現れるまで、よく因数分解された一つのモジュールを保ちます。同じロジックがサインアップと課金のフローで三度目に現れたとき、憶測によるフレームワークではなく、小さな共有関数を一つ取り出します。これでコードベースは、誰もが頭に収められるほど小さく保たれ、彼らが引く少数の継ぎ目は、製品が最も変わりそうな所に落ちます。
大企業。 大手の保険プラットフォームは、契約、保険金請求、課金を別々の境界付けられたコンテキストとしてモデル化し、それぞれが独自のデータモデルとサービス境界を持つ専任のチームに所有されています。請求が契約を参照するように、コンテキストが出会う所では、共有のデータベーステーブルではなく、明示的に公開された契約を通じて話します。これにより三つのチームは独立して進化でき、ユビキタス言語が、引受担当者や保険数理士との会話を正確に保ちます。以前のバージョンは一つの広がった共有モデルを使っていて、あらゆる変更にチーム間の調整が必要でした。
政府。 国の税処理システムは、計算エンジンに、意図してデータ指向で関数型のコアを選びます。税のルールは、不変の入力レコードに対する純粋な変換として表現され、ある課税年度について、監査可能でテスト可能で再現可能になります。命令型で状態を持つ部分(ワークフロー、通知)は周縁に置かれます。監査人は特定のルールのバージョンを指して、過去の計算を正確に再現でき、これは、隠れた可変状態を持つ絡み合ったオブジェクトグラフには保証できなかった法的要件です。
ビジネスケース: 動機、ROI、TCO
設計の質は、システムの変更容易性への投資であり、変更容易性が総所有コストを支配します。システムのコストの大半は最初のリリースの後、変更と拡張で発生します。よく設計されたシステムは、変更のコストを時間とともにほぼ一定に保ちます。まずく設計されたシステムでは、各変更のコストが上昇し、システムが事実上変更不能になって書き直さなければならなくなります。これは最も高価な結果です。
採用のコストは、おもにスキルとレビューの規律です。原則を教えることと、前もって設計の時間を使うこと。採用しないコストは、技術的負債の緩やかな蓄積、低下するデリバリーのベロシティ、上昇する欠陥率、そして最終的な高価な書き直しです。リーダーシップに論拠を示すには、設計の規律を、デリバリーの予測可能性と書き直しプログラムの回避に結びつけ、変更失敗率や、比較できる機能を実装する時間のような先行指標を、時間にわたって追跡してください。反対の失敗にも注意してください。不確かな未来のために設計に過剰投資することもまた、価値を破壊します。だから論拠は、将来の変更がどれだけ起こりやすく、どれだけコストがかかるかに合わせて調整された、適切な設計のためのものです。
アンチパターンと落とし穴
- 憶測による一般性: 決して来ない想像上の要件のために拡張性を作ること。
- 誤った抽象化: DRYを満たすために無関係なコードを無理にまとめ、重複より悪い結合を作ること。
- パターンの盲信: それ自体のためにデザインパターンを適用し、利益のない間接層を加えること。
- 貧血モデルまたはゴッドオブジェクト: 振る舞いのないモデル、あるいは何でもするオブジェクト。どちらも責任の置き場所の誤りを示します。
- 分散モノリス: 物理的に分割されながら、依然として密に結合したサービス。両方のアプローチのコストを兼ね備えます。
- ビッグボールオブマッド: 見分けられる構造がなく、あらゆる変更がすべてを危険にさらすこと。
- DDDの芝居: 価値を与えるドメインのモデル化なしに、語彙とフォルダ構造だけを採用すること。
成熟度モデル
- レベル1、開始: 設計は場当たり的で反応的です。結合は歯止めなく蓄積し、原則は知られていないかスローガンとして引き合いに出され、抽象化は個人の癖で現れたり消えたりします。
- レベル2、発展: チームは原則を知っていて適用しますが、一貫せず、しばしば教条的です。結合度と凝集度を意図して管理するグループもあればそうでないグループもあり、組織全体で共有された語彙はありません。
- レベル3、標準化: 共有された設計の語彙、抽象化を取り出すための三回ルール、結合度と凝集度の分析、チームに揃えられた境界付けられたコンテキストが、文書化され、組織全体で期待され、個人の好みに任されず、設計レビューで一貫して適用されます。
- レベル4、管理: 設計の健全性がベースラインに対して測定されます。結合と共変更のデータ、変更失敗率、比較できる機能を実装する時間が時間とともに追跡され、抽象化と境界が証拠に基づいて追加され、保たれ、取り除かれ、過剰設計と誤った抽象化が、意見ではなくデータで捉えられます。
- レベル5、オーケストレーション: 設計の規律が、組織全体でデリバリーとリスク計画に統合されています。原則は繊細さと既知の失敗モードをもって適用され、パラダイムと境界の選択は意図的で継続的に見直され、組織はドメインと証拠の変化に応じて、抽象化を日常的にリファクタリングし、スコープを見直し、廃止します。
議論のためのアイデア
- 将来の要件を持つ前に、必要な継ぎ目と憶測による一般性の違いをどう見分けますか。
- DRYがあなたのチームを誤った抽象化に導いたのはいつで、どう認識しましたか。
- 境界付けられたコンテキストの境界はどこに落ちるべきで、組織図をどれだけ忠実に映すべきですか。
- あなたの文脈では、コードの前にどれだけの設計があるべきで、決定をどう記録しますか。
- システムのどの部分が、より関数型またはデータ指向のスタイルから恩恵を受けますか。
- 設計原則が、実用的な例外に抵抗する教条に固まらないようにするには、どうしますか。
要点
- 重要なのは結合度と凝集度という特性であり、頭字語はその目的への手段です。
- すべての原則に失敗モードがあります。それぞれがいつ誤導するかを知ってください。
- 早すぎる、あるいは誤った抽象化より、少しの重複を好みます。
- DDDと境界付けられたコンテキストを使って、複雑なドメインをチームのオーナーシップに揃えます。
- パラダイムは適合性で選びます。大きなシステムは実用的に混在しています。
- 実際に必要になる変更のために設計し、設計不足と過剰設計の両方を避けます。
参考文献とさらなる読み物
- Robert C. Martin, Clean Architecture and Agile Software Development, Principles, Patterns, and Practices
- Eric Evans, Domain-Driven Design: Tackling Complexity in the Heart of Software
- Vaughn Vernon, Implementing Domain-Driven Design
- Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software
- Martin Fowler, Refactoring: Improving the Design of Existing Code and Patterns of Enterprise Application Architecture
- David L. Parnas, On the Criteria to Be Used in Decomposing Systems into Modules
- Sandi Metz, Practical Object-Oriented Design