プログラミング原則

目次

主要項目のみを表示しています。詳細な小見出しは本文内で確認できます。

概要

プログラミング原則は、設計や実装で繰り返し現れるトレードオフに名前を与えた経験則の集合です。デザインパターンが「具体的な構造のテンプレート」であるのに対し、原則は「どちらに寄せるかを決めるための判断軸」を提供します。

要点

プログラミング原則は、コードを書く前に守るべき戒律ではなく、設計判断のときに参照する語彙です。SOLIDやDRYのような名前が付いていることで、コードレビューや議論で「どの軸の問題か」を短く共有でき、暗黙の前提を可視化できます。原則同士はしばしば衝突するため、状況に応じて優先順位を選ぶ必要があります。

flowchart LR P["設計判断"] --> R["責務<br>SRP / SoC / LoD"] P --> C["変更容易性<br>OCP / DIP / DRY"] P --> S["単純さ<br>KISS / YAGNI / 3度目の法則"] P --> Q["予測可能性<br>POLA / 早く失敗せよ / 規約"]

プログラミング原則とは

プログラミング原則は、ソフトウェア設計や日々のコーディングで参照される名前付きの指針です。SOLIDのように体系化されたものから、DRYKISSのような短いスローガンまで、粒度はさまざまです。

代表的な性質は次の通りです。

  • 特定の言語や環境に依存しない
  • 実装の詳細ではなく、設計上の力のかかり方を扱う
  • 名前があるため、レビューや議論で短く参照できる
  • 単独では適用できず、他の原則と組み合わせて判断する
  • 多くは「絶対の真理」ではなく経験則である

UNIX哲学が「ツールを設計するための文化的指針」であったように、プログラミング原則は「クラス、関数、モジュール、サービスを設計するための判断軸」です。

なぜ原則として学ぶのか

原則を学ぶ価値は、暗記して機械的に当てはめることではなく、設計の典型的なトレードオフを理解することにあります。

原則を知ると、次を考えやすくなります。

  • どの責務が一つのクラスに混ざっているか
  • 変更を受け入れる境界はどこに置くべきか
  • どこに重複が許され、どこに重複が痛みを生むか
  • 早く作る価値と、未来を見越す価値はどちらが大きいか
  • 利用者の予測を裏切らないインターフェースとは何か

原則は、設計を難しくするためのルールではなく、判断の材料を増やすための語彙です。新しい原則を学ぶたびに、既存のコードに対して別の角度の問いを立てられるようになります。

原則全体の地図

プログラミング原則は分類の仕方が一つではありませんが、扱う関心ごとでまとめると見通しがよくなります。

関心 主な原則
責務の分離 SRP / SoC / LoD
変更容易性 OCP / DIP / DRY / 継承より合成
インターフェース設計 ISP / 尋ねるな、命じよ / POLA / 設定より規約
単純さと過剰設計の回避 KISS / YAGNI / 3度目の法則
信頼性と検出可能性 早く失敗せよ / 信頼できる唯一の情報源
継続的な改善 ボーイスカウト・ルール

すべてを毎回意識する必要はありません。設計上の問いが立ち上がったときに、関連する原則を引き出せれば十分です。

オブジェクト指向設計の五原則

オブジェクト指向設計の五原則(SOLID: SOLID Principles)は、Robert C. Martin(Uncle Bob)が2000年の論文「設計原則とデザインパターン」でまとめた5つのオブジェクト指向設計原則の頭文字です。SOLIDという略語自体は2004年頃にMichael Feathersが命名したと言われています。

5つの原則は次の通りです。

略語 名称 短い言葉
S 単一責任原則 クラスが変更される理由は一つに
O 開放閉鎖原則 拡張に開き、変更に閉じる
L リスコフの置換原則 派生型は基底型と置換可能
I インターフェース分離の原則 必要のないインターフェースに依存させない
D 依存関係逆転の原則 抽象に依存し、具象に依存しない

SOLIDはクラス指向のオブジェクト指向設計を念頭に作られましたが、現代では関数型やマイクロサービスにも応用されます。重要なのは、5つを単独に当てはめるより、互いに支え合う原則として捉えることです。

単一責任原則

単一責任原則(SRP: Single Responsibility Principle)は、Martinの原典では「クラスが変更される理由は一つだけであるべき」と表現されます。

「責務」は機能の数ではなく、変更を要求する人や理由のことです。たとえば、Report クラスが「集計ロジック」「画面表示」「PDF出力」を全部抱えていると、表示を変えたい人と、集計式を変えたい人と、PDFのレイアウトを変えたい人の3者が同じクラスを編集することになります。これは衝突や副作用の温床になります。

SRPに沿うと次のように分けられます。

class ReportCalculator:
    def calculate(self, data): ...

class ReportPresenter:
    def render(self, result): ...

class ReportPdfWriter:
    def write(self, result, path): ...

注意点もあります。SRPを極端に追うとクラスが増えすぎ、全体の流れを追いにくくなります。「変更理由が同じものは同じ場所に置く」という積極的な側面と合わせて考えるとバランスが取れます。

開放閉鎖原則

開放閉鎖原則(OCP: Open Closed Principle)は、Bertrand Meyerが1988年に「オブジェクト指向ソフトウェア構築」で提唱したものを、Martinが具体的なクラス設計の文脈に適用し直しました。原典の表現は「ソフトウェアの構成要素は、拡張に対して開かれ、変更に対して閉じられているべき」です。

意図は「動いているコードを書き換えずに、新しい振る舞いを追加できるようにする」ことです。条件分岐を増やすのではなく、新しい型を追加することで拡張する設計を志向します。

class Shape:
    def area(self) -> float: ...

class Circle(Shape):
    def area(self): return 3.14 * self.r ** 2

class Square(Shape):
    def area(self): return self.side ** 2

新しい形状を追加したいときは、Shape を継承する新しいクラスを作るだけで済みます。既存のクラスに if shape == "triangle": ... を足す形にはなりません。

ただし、すべての箇所をOCPで拡張可能にすると、過剰な抽象化と複雑さを招きます。「変更が起きそうな軸」を見極めて、その軸だけ拡張点を持たせるのが現実的です。

リスコフの置換原則

リスコフの置換原則(LSP: Liskov Substitution Principle)は、Barbara Liskovが1987年の講演「データ抽象と階層」で示し、Martinが「基底クラスを使う関数は、派生クラスのオブジェクトをそれと知らずに使えなければならない」と整理しました。

基底型を引数に取る関数に派生型を渡しても、契約上の振る舞いが壊れないことを要求します。よく出てくる反例は Square extends Rectangle です。Rectangle には「幅と高さを独立に変えられる」という暗黙の契約がありますが、Square ではそれが成立しません。継承関係を作っても、置換できなければLSPに反します。

LSPは事前条件と事後条件の概念とも結びつきます。

  • 派生型は事前条件を強めてはいけない
  • 派生型は事後条件を弱めてはいけない
  • 派生型は基底型の不変条件を保つ

LSPに違反するクラス階層は、利用側で型ごとの分岐が必要になり、OCPも壊します。継承を選ぶ前に、本当に置換可能かを問うとよい設計判断ができます。

インターフェース分離の原則

インターフェース分離の原則(ISP: Interface Segregation Principle)は「クライアントは、自分が使わないインターフェースへの依存を強制されるべきではない」という原則です。一つの巨大なインターフェースに依存させず、利用者ごとに必要なメソッドだけを切り出します。

class Printer:
    def print(self, doc): ...

class Scanner:
    def scan(self): ...

class Fax:
    def send_fax(self, doc): ...

これらをまとめた MultiFunctionDevice のような巨大インターフェースを使う代わりに、利用側は必要な能力のインターフェースだけに依存します。実装側は複数のインターフェースを束ねれば済みます。

ISPは、Goのインターフェースのように「必要なメソッドを利用側で小さく定義する」スタイルとよく合います。Javaのように事前にインターフェースを宣言する場合でも、太ったインターフェースを切り分けることでテストや差し替えがしやすくなります。

依存関係逆転の原則

依存関係逆転の原則(DIP: Dependency Inversion Principle)は、二つの命題からなります。

  1. 上位モジュールは下位モジュールに依存してはならない。両者は抽象に依存すべきである
  2. 抽象は詳細に依存してはならない。詳細は抽象に依存すべきである

短くは「具象ではなく抽象に依存せよ」です。

flowchart LR A["UserService<br>(上位)"] -->|依存| I["UserRepository<br>(抽象)"] P["PostgresUserRepository<br>(下位)"] -->|実装| I

UserServicePostgresUserRepository を直接 import するのではなく、両方が UserRepository というインターフェースに依存します。これにより、データベースを差し替えてもサービス層は変更不要です。

DIPは依存性注入(DI)と密接ですが、同じものではありません。DIは依存を外から渡す技法、DIPは「どの方向に依存すべきか」という設計原則です。

DRY原則

知識の重複を避ける(DRY: Don’t Repeat Yourself)は、Andy HuntとDave Thomasの「達人プログラマー」で提唱されました。原典の表現は「すべての知識は、システム内で単一かつ曖昧でない、信頼できる表現を持つべき」です。

注意したいのは、DRYが扱うのは「コードの重複」ではなく「知識の重複」だという点です。同じ計算式やルールが複数の場所に書かれていると、片方だけ更新されたときに整合性が崩れます。一方、見た目が似ているだけで、変更理由が異なる二つのコードは、無理に統合するとかえって脆くなります。

DRYは次のような場面で特に重要です。

  • ビジネスルールの定義
  • 設定値や定数の参照
  • データベーススキーマと型定義
  • ドキュメントとコードの仕様
  • ビルド設定とCIパイプライン

逆に、テスト用のセットアップコードや、アーキテクチャ層をまたぐ似たデータ構造(DTOとEntity)は、形が似ていても意図が異なるため、DRYを過剰に適用すると結合度が上がります。「同じ知識か、似た見た目か」を見極める力がDRYの肝です。

KISS原則

単純に保つ(KISS: Keep It Simple, Stupid)は、米国海軍が1960年に言及し、Lockheed Skunk Worksの主任技術者Kelly Johnsonが社内のスローガンとして広めたとされます。航空機が「平均的な整備士が、限られた工具で野戦の現場でも修理できる」設計であるべきだ、という発想がもとです。

ソフトウェアでも同じです。賢い実装より、誰でも理解して直せる実装が長期的には強くなります。

KISSが避けようとするのは次のようなものです。

  • 過剰な抽象化
  • 複雑な継承階層
  • 早すぎる最適化
  • 多機能化したフラグや設定
  • 利口だが追いにくいワンライナー

ただし、「単純」と「素朴」は違います。本質的に複雑な問題に対して安易な実装を選ぶと、別の場所に複雑さを押し付けることになります。KISSの「単純さ」は、見た目の短さではなく、説明と修正のしやすさです。

YAGNI原則

今必要ないものは作らない(YAGNI: You Aren’t Gonna Need It)は、エクストリームプログラミング(XP)の文脈で生まれました。Kent BeckとChet HendricksonがクライスラーのC3プロジェクトで議論する中で、Chetが「将来必要になるはずの機能」を提案するたびにKentが「それは必要にならない」と返したことが起源と伝えられます。Beckの「エクストリームプログラミング入門」で正式に文書化されました。

YAGNIは「今の要件に対して最小限の実装で動くものを作る」ことを推奨します。Martin Fowlerは、推測で作る機能がもたらす4つのコストを整理しています。

コスト 内容
構築コスト 不要な機能の分析・実装・テストに使った時間
遅延コスト その時間で作れたはずの本当に必要な機能を逃す
持ち運びコスト 余分な機能が他の変更を難しくする
修理コスト 半年後に「正しい設計」を再実装し直す手間

YAGNIは「設計を考えるな」ではありません。今の要件に必要な抽象は作るが、「いつか必要かもしれない」拡張点は今は作らない、ということです。実際に二度目の変更が来たときに、本当に必要な抽象が見えるためです。

デメテルの法則

デメテルの法則(LoD: Law of Demeter)は、別名「最小知識の原則」とも呼ばれ、1987年にIan Hollandが、北米Northeastern大学のLieberherrらと共にDemeterプロジェクトの中で提唱しました。原典は1989年のACM SIGPLAN Noticesにあります。

簡単な言い回しは「直接の友達としか話すな」です。

具体的には、あるメソッドが呼んでよいのは次の対象のみとします。

  • 自分自身のメソッド
  • 引数として渡されたオブジェクトのメソッド
  • 自身が生成したオブジェクトのメソッド
  • 自身が直接保持しているフィールドのメソッド
# LoD違反
total = order.customer.address.city.tax_rate * order.amount

# 改善
total = order.tax_for_amount()

order.customer.address.city のような長い連鎖は、内部構造を呼び出し側にさらしています。顧客や住所の構造を変えると、すべての呼び出し元が壊れます。

LoDの本質は「責務を持つオブジェクトに仕事を任せる」ことです。Tell Don’t Askと深く結びついています。一方で、配列やマップのようにデータコンテナに対して長い式を書くのは普通であり、すべての連鎖がLoD違反というわけではありません。

継承より合成

Composition over Inheritance(継承より合成)は、Gang of Fourの「デザインパターン」序章で示された「クラス継承よりオブジェクト合成を優先せよ」という設計原則です。

継承は強力ですが、次のような問題があります。

  • 親クラスの変更が子クラスを壊す(脆い基底クラス問題)
  • 実行時に振る舞いを差し替えにくい
  • 多重継承が複雑になりやすい
  • 継承階層が深くなると追えなくなる

合成は「持つ」関係で機能を組み立てます。ストラテジーパターンやデコレータパターンが典型例です。

class Logger:
    def __init__(self, formatter, sink):
        self.formatter = formatter
        self.sink = sink
    def log(self, msg):
        self.sink.write(self.formatter.format(msg))

Logger を継承するのではなく、formattersink を差し替えることで多様な振る舞いを実現できます。

ただし、継承が常に悪いわけではありません。「である」の関係が真で、振る舞いの置換可能性(LSP)も保たれるなら、継承は短く強力な道具です。継承より合成は「迷ったら合成を選べ」という指針であり、継承を禁止する原則ではありません。

尋ねるな、命じよ

Tell Don’t Ask(尋ねるな、命じよ)は、Alec Sharpが1997年の「例で学ぶSmalltalk」で示し、「達人プログラマー」でも紹介された原則です。Sharpの言葉では「手続き型コードは情報を取り出して判断する。オブジェクト指向コードはオブジェクトに仕事を依頼する」です。

# Ask(情報を取り出して呼び出し側で判断)
if account.balance >= amount:
    account.balance -= amount

# Tell(オブジェクトに依頼する)
account.withdraw(amount)

問い合わせの形は、Account の内部状態を呼び出し側にさらしています。残高チェックのルールが変わるたびに呼び出し側を書き換える必要があり、整合性も壊れやすくなります。

Tell Don’t Askは、オブジェクトを「データの入れ物」ではなく「振る舞いと不変条件を守る主体」として扱う考え方です。Law of Demeterや Encapsulation と同じ方向を向いています。

ただし、すべての場面で適用できるわけではありません。クエリやレポートのように、データを集めて表示する処理ではAskの形が自然です。Tell Don’t Askは「ドメインの判断を呼び出し側に漏らすな」というニュアンスで使うと過剰適用を避けられます。

関心の分離

関心の分離(SoC: Separation of Concerns)は、Edsger Dijkstraが1974年の論文「科学的思考の役割について」(EWD447)で示した考え方です。「同じものを別の側面から見るとき、それぞれの側面を独立に扱えるようにすべき」という命題で、ソフトウェアに限らない普遍的な思考原則として書かれました。

Dijkstraは「正しさ」と「効率」を例に挙げ、これらを同時に考えると思考が混ざってしまうので、別々に整理することで効果的に扱えると述べました。

ソフトウェアでは次のような分離が典型です。

  • 表示と業務ロジックの分離(MVC、MVVM)
  • 認証と認可の分離
  • ドメインロジックと永続化の分離
  • ビジネスルールとUIの分離
  • 設定と実行の分離

SoCはSRPと混同されがちですが、層の粒度が違います。SRPはクラス単位の指針、SoCはアーキテクチャ全体に対する設計思想です。「どの軸で切り分けると、変更が独立に進められるか」を考えるための原則です。

信頼できる唯一の情報源

信頼できる唯一の情報源(SSOT: Single Source of Truth)は、データやルールが一箇所にだけ存在し、他の場所はそれを参照する設計原則です。データベース設計、設定管理、ドキュメント運用にも適用されます。

具体例を挙げます。

  • ユーザーIDのフォーマット規則は User モデルにだけ定義する
  • 環境設定は .env か設定サービスに集約し、各サービスが参照する
  • API仕様はOpenAPI定義から型とドキュメントを生成する
  • データベーススキーマから型定義を派生させる

SSOTを破ると、複数の箇所に同じ知識が散らばり、片方だけ更新される可能性が常に存在します。これは事故の原因になります。

DRYと近い概念ですが、SSOTはランタイムのデータや構成情報まで含む、より広い範囲を指します。「真実が複数あるとき、どれを信じるか」を決める設計判断です。

最小驚きの原則

最小驚きの原則(POLA: Principle of Least Astonishment)は、POLSとも呼ばれ、利用者が予想する振る舞いから外れた挙動を避けるべきだという原則です。1967年のPL/Iのドキュメントで「最小驚きの法則」として早期に言及され、1972年のプログラミング言語設計の論文で明示的に整理されました。

POLAが扱うのは次のような問題です。

  • 同名の関数が言語によって異なる動作をする
  • 設定の優先順位が直感に反する
  • 副作用が見えない場所で起きる
  • エラーメッセージが原因と関係ない情報を返す
  • ショートカットキーが他のアプリと違う

「驚かせない」とは、利用者の既存のメンタルモデルを尊重するという意味です。新しい設計をするときも、既存の慣習やライブラリの規則を踏襲することで、学習コストを下げられます。

POLAはAPI設計、CLI設計、UI設計のすべてに効きます。コンベンションを破る場合は、それを補うだけの利益とドキュメントが必要です。

設定より規約

設定より規約(CoC: Convention over Configuration)は、David Heinemeier Hansson(DHH)が2004年にRuby on Railsで広めた設計思想です。明示的な設定を書かなくても、合理的な既定値が動くようにすることで、開発者が意思決定の負担から解放されます。

Railsでは、ファイル名、テーブル名、クラス名の対応が規約として定められています。

  • User クラスは users テーブルに対応する
  • UsersControllerapp/controllers/users_controller.rb に置く
  • ビューは app/views/users/index.html.erb

これにより、フレームワークの利用者は「どこに何を置くか」を考えずに本質に集中できます。

設定より規約の強みは、既定がうまく合うときに圧倒的な生産性を発揮することです。一方、規約と異なるニーズが出てきたときには、規約を上書きするコストが上がります。柔軟性と一貫性のトレードオフを意識する必要があります。

POLAやKISSと相性がよく、UNIX哲学の 驚き最小 ともつながります。

早く失敗せよ

Fail Fast(早く失敗せよ)は、Jim ShoreがMartin Fowlerの編集する「IEEE Software」の連載「設計」で2004年に発表した原則です。「失敗を遅らせるな、すぐ失敗して原因が見えるところで止めろ」という思想です。

具体的には次のような実装が含まれます。

  • 不正な引数は呼び出し直後に例外で弾く
  • 起動時に設定の整合性を検証する
  • アサーションで前提条件を確認する
  • nullや想定外の状態を黙って通過させない
  • 検証は深くなる前に行う
def transfer(amount, from_account, to_account):
    if amount <= 0:
        raise ValueError("amount must be positive")
    if from_account is None or to_account is None:
        raise ValueError("accounts must not be None")
    ...

早く失敗せよの利点は、バグが発生した瞬間にスタックトレースが原因のすぐ近くを指すことです。エラーがデータの奥深くまで伝播してから顕在化すると、原因の特定が困難になります。

ただし、利用者向けのインターフェースでは、早く失敗せよと「親切なエラーハンドリング」のバランスが必要です。バックエンドの内部処理では早く失敗し、ユーザーが触る入力では検証してからメッセージを返す、という使い分けが現実的です。

ボーイスカウト・ルール

Boy Scout Rule(ボーイスカウト・ルール)は、「コードを見つけたときより少しきれいにして去る」という指針です。Robert C. Martinが「プログラマが知るべき97のこと」で広めました。元になったのは、ボーイスカウトの創始者Robert Baden-Powellの「この世界を見つけたときより少し良くして去るよう努めよ」というスカウト哲学です。

具体的には、コードに触れたついでに次のような小さな改善を加えます。

  • 意図不明な変数名を直す
  • 重複したコードを抽出する
  • 死んだコードを消す
  • マジックナンバーに名前を付ける
  • 古いコメントを更新するか削除する

大きなリファクタリングを別タスクとして計画するより、日常の変更の中で少しずつ片付ける方が、技術的負債は溜まりにくくなります。

ボーイスカウト・ルールの注意点は、レビュー対象が肥大化することと、変更理由が不明確になることです。「同じPRで同時に直してよいか」のラインはチームで決めるとよいです。

3度目の法則

Rule of Three(3度目の法則)は、Don Robertsが提唱し、Martin Fowlerが「リファクタリング」(1999)で広めた経験則です。原典の言い回しは次の通りです。

最初に何かをするときは、そのままやる。2回目に似たことをするときは、重複に顔をしかめつつ、それでも重複したまま進める。3回目に似たことをするときに、初めてリファクタリングする。

要するに「最初は書いてしまえ。2回目に重複に気付いても、まだ抽象化するな。3回目に同じことが現れたら、初めて共通化する」という指針です。

Rule of Threeが効くのは、抽象化を急ぎすぎると間違った抽象を選ぶ危険があるためです。最初の2例だけでは、本当に共通化すべき軸が見えていないことが多いのです。3例集めることで、ようやくパターンの形が浮かび上がります。

DRYと衝突するように見えますが、実は補完的です。DRYは「知識を一箇所に」と言い、Rule of Threeは「重複を共通化するタイミング」を提示します。両者を合わせると、「重複は悪いが、急ぐ前に観察せよ」という穏当な指針になります。

ハリウッド原則と制御の反転

Hollywood Principle(ハリウッド原則)は、「こちらから連絡しない、こちらから呼び出す」という映画業界の決まり文句に由来します。制御の反転(IoC: Inversion of Control)は、プログラムが直接ライブラリを呼ぶのではなく、フレームワーク側がプログラムを呼ぶ、という制御の流れの逆転を表します。

これは Inversion of Control(IoC、制御の反転)とも呼ばれ、語源は1988年頃のフレームワーク設計の議論にさかのぼります。Michael Jacksonが1970年代の「Jackson構造化プログラミング」で論じた「プログラム反転」と密接な関係があります。

# 通常の制御フロー(プログラムがライブラリを呼ぶ)
def main():
    data = load_data()
    result = process(data)
    save(result)

# IoC(フレームワークがプログラムを呼ぶ)
@app.route("/process")
def handler(request):
    return process(request.data)

IoC を実現する具体的な仕組みは複数あります。

  • イベントループとコールバック
  • 依存性注入(DI)
  • サービスロケータ
  • テンプレートメソッドパターン
  • フレームワークによるライフサイクル管理(onCreate, onStart, onStop)

メリット:

  • フレームワークと拡張点の責務が明確になる
  • 利用側はビジネスロジックに集中できる
  • テスト時にコンポーネントを差し替えやすい
  • 結合度が下がる

デメリット:

  • 制御フローが見えにくくなる(フレームワーク内部の理解が必要)
  • 学習コスト
  • デバッグの追跡が難しい

ハリウッド原則は、Spring、Django、Rails、React、Angular、Express などの現代フレームワークで実質的に標準となっています。DIP(依存関係逆転の原則)と組み合わせて使うと、テスタブルで疎結合な設計が実現できます。

責務割り当ての原則

GRASP(General Responsibility Assignment Software Patterns、一般責務割り当てソフトウェアパターン)は、Craig Larman が1997年の「UMLとパターンの適用」で提示した9つの基本原則です。SOLIDより細かい粒度で、「どのクラスにどの責務を割り当てるか」を考える際の指針を提供します。

パターン 概要
情報エキスパート 情報を持っているクラスに責務を持たせる
生成者 生成元の関係(包含、集約)に基づいて生成責務を決める
コントローラ システム外からの入力を受ける窓口を1つ作る
低結合 クラス間の依存を最小化する
高凝集 クラス内の責務をまとめる
多態性 型による振る舞い変化を多態性で表現する
純粋人工物 適切な置き場所がない責務を持つ人工クラスを作る
間接化 中間クラスで結合を弱める
変更からの保護 変更を予想して安定インターフェースで隠す

情報エキスパートは最も基本となる原則です。Order の合計金額は誰が計算すべきか、と問われたら、OrderLine の集合を持っている Order 自身、と答えるのが情報エキスパートの発想です。データのある場所で計算する、という原則です。

GRASP は SOLID と矛盾しません。むしろ、SOLID が「どう設計するか」を抽象的に示すのに対し、GRASP は「具体的にどの責務をどのクラスに置くか」というクラス設計の現場で使える手順を示します。

情報隠蔽

Information Hiding(情報隠蔽)は、David Parnas が1972年の Communications of the ACM 論文「システムをモジュールに分解するときの基準」で提唱した、モジュール設計の根本原則です。

各モジュールは、変更されうる設計判断を他から隠すように設計する。

Parnas は、モジュール分割の基準として「処理の流れ」ではなく「変更されうる設計判断の単位」を用いることを提唱しました。各モジュールは、自分が抱える設計判断(データ構造、アルゴリズム、I/O 形式など)を他のモジュールに見せないインターフェースを持ちます。

例:

  • ハッシュテーブルの実装が変わっても、利用側は影響を受けない
  • データベースの種類が変わっても、リポジトリ層が吸収する
  • ファイル形式が変わっても、シリアライザが吸収する

情報隠蔽はカプセル化と密接ですが、概念としては別です。カプセル化は「データとふるまいをまとめる」言語機能、情報隠蔽は「変更を吸収するためにインターフェースを設計する」設計原則です。

OCPやDIPの基礎となる考え方であり、現代のモジュール設計、API設計、マイクロサービス境界の設計でも生きています。

高凝集・低結合

High Cohesion / Low Coupling(高凝集・低結合)は、GRASPの2つを単独項目として扱うものです。これは古典的な構造化プログラミング時代から続く設計原則で、Larry Constantineが1970年代にIBM のシステム設計で提唱したとされます。

  • 凝集度(Cohesion): モジュール内の要素が、どれだけ強く関連しているか
  • 結合度(Coupling): モジュール間の依存が、どれだけ強いか

凝集度は高く、結合度は低く、というのが理想です。

凝集度のレベル(Constantineが分類した7段階):

レベル 性質 望ましさ
機能的凝集 単一機能のために協調 最も望ましい
逐次的凝集 出力が次の入力になる 良い
通信的凝集 同じデータに対して操作 まあ良い
手続き的凝集 同じ手順の中で実行 やや弱い
時間的凝集 同じタイミングで実行 弱い
論理的凝集 カテゴリだけ似ている 悪い
偶発的凝集 偶然集まった 最悪

結合度のレベル:

レベル 性質 望ましさ
データ結合 必要なデータだけを引数で渡す 最も良い
スタンプ結合 構造体を渡す 良い
制御結合 フラグで挙動を制御する やや弱い
外部結合 外部フォーマットに依存 弱い
共通結合 グローバルデータを共有 悪い
内容結合 他モジュールの内部に直接アクセス 最悪

実務では、これらを厳密に分類するより、変更したときに他に波及するか 理解するために他のモジュールを読まないといけないか という問いで判断するのが実用的です。

悪い方が良い

Worse is Better(悪い方が良い)は、Richard P. Gabriel が1991年のエッセイ「悪い方が良いの台頭」で提示した、ソフトウェアの設計哲学に関する観察です。

Gabriel は二つの設計スタイルを対比しました。

観点 正しいもの(MIT/Stanford流) 悪い方が良い(New Jersey流)
単純さ 実装と利用者インターフェースの両方が単純であるべき。利用者インターフェースの単純さが優先 実装の単純さが最重要。利用者インターフェースは多少犠牲にしてもよい
正しさ 正しさが最優先 やや単純化された方が良い場合がある
一貫性 一貫性が重要 一貫性は単純さより低優先
完全性 あらゆる場合をカバー 単純さのために省く

UNIX と C 言語が広まった理由として、Gabriel はこれを挙げます。「悪い方が良い」のアプローチは「動くものを早く出して、現場のフィードバックで改善する」傾向があり、結果として広く採用されました。

ただし、Gabriel 自身もこの観察に複雑な感情を表明しています。「悪い方が良いのは、やはり悪い」という後続のエッセイもあり、設計哲学としての結論は単純ではありません。

実務では、「正しいもの」を目指して動かないものを作るより、「悪い方が良い」で動くものから始める方が多くの場合は成功する、という観察として読まれます。ギャルの法則やYAGNIと相性がよい考え方です。

ロバストネス原則

Robustness Principle(ロバストネス原則、ポステルの法則)は、ソフトウェアの法則の章でも扱いましたが、原則としても重要なので再度整理します。

送るものには厳密に、受け取るものには寛容に。

1980年の RFC 761(TCP仕様)で Jon Postel が記した一文です。

設計原則としての含意:

  • API の出力は仕様通りに厳密に
  • 入力の検証は寛容で、軽微な逸脱を許容する
  • ただし、寛容さが「曖昧さの容認」になってはいけない

近年の批判(Martin Thomson と David Schinazi、2023年)は、「寛容な受信は仕様違反を温存し、長期的にプロトコル全体を弱める」と指摘します。Webブラウザが寛容なHTMLパーサを持ったために、世界中のHTMLが「壊れている方が普通」になった歴史が代表例です。

現代のプラクティスは、状況に応じて使い分ける形です。

  • セキュリティ重要な領域: 厳密に検証、早く失敗せよ
  • 後方互換性が重要な領域: ポステルの法則を緩く適用
  • 公開API: 入力検証を厳密、出力を仕様通り

銀の弾丸はない

No Silver Bullet(銀の弾丸はない)は、Fred Brooks の1986年の論文「銀の弾丸はない: ソフトウェア工学における本質と偶然」で示された、ソフトウェア開発の本質的な複雑さを論じる古典的な考え方です。Brooks は二種類の複雑さを区別しました。

  • 本質的複雑さ: 解決しようとする問題そのものが持つ複雑さ
  • 偶発的複雑さ: 道具や言語、環境に起因する複雑さ

Brooks の主張は、「向こう10年で生産性を10倍にする銀の弾丸はない」というものでした。なぜなら、近代のツールやプログラミング言語は偶発的複雑さの大部分を解消しており、残るのは本質的複雑さで、それは魔法では消せないからです。

この観察は、原則というよりも開発戦略の指針です。

  • 速度を求めるなら、本質的複雑さを正しく理解することにエネルギーを使う
  • ツールやフレームワーク選定は、偶発的複雑さの削減に効く
  • 完璧な設計を待つより、本質を学びながら反復する

「人月の神話」全体の主題ともいえる考え方で、現代のソフトウェア開発でも引用され続けています。

ハイラムの法則

Hyrum’s Law(ハイラムの法則)は、ソフトウェアの法則の章で扱ったものですが、原則としても示唆を持ちます。

設計判断としての含意:

  • API は出力を意図的に乱雑化する(順序のシャッフル、タイミングのジッタ)
  • バージョンを上げる前に行動の差分を観測する
  • 内部実装を意図的に隠す(構造体ではなく不透明なIDを返す)
  • ドキュメントよりも実装が依存される現実を受け入れる
  • フェーズドロールアウトとカナリアリリースで影響を観測する

これは情報隠蔽と密接で、「変更したいと思ったときに変更できる設計」を支える基盤です。

モジュールの凝集

Modular Cohesion(モジュールの凝集)は、Yourdon と Constantine の「構造化設計」(1979)で詳述された原則です。モジュール内の要素が一つの目的に貢献していることを目指します。

実装の指針:

  • 一つのモジュールは一つの抽象を表現する
  • フィールドとメソッドが互いに関連している
  • 外部から観察したとき、モジュールが何をするかを一文で説明できる
  • モジュール内のコードを移動しやすい

凝集度が低いモジュールは、神オブジェクト(アンチパターン)や機能の横恋慕(コードスメル)として現れます。

カプセル化原則

Encapsulation Principle(カプセル化原則)は、データとそれを操作するふるまいを一つの単位にまとめ、内部状態を外部から直接操作させない原則です。OOP の基本柱の一つで、情報隠蔽と相補的に機能します。

# カプセル化が弱い
class Account:
    def __init__(self):
        self.balance = 0

# カプセル化が強い
class Account:
    def __init__(self):
        self._balance = 0

    def withdraw(self, amount):
        if amount > self._balance:
            raise InsufficientFunds()
        self._balance -= amount

    def deposit(self, amount):
        if amount <= 0:
            raise InvalidAmount()
        self._balance += amount

    @property
    def balance(self):
        return self._balance

カプセル化が強いと、不変条件(balance は0以上、引き出し額は残高以下、入金額は正)をクラス内で守れます。「尋ねるな、命じよ」との関係も深く、外部から状態を聞いて判断する代わりに、メソッドに依頼する形を取ります。

原則同士の関係

プログラミング原則は孤立して存在するわけではなく、互いに補強しあったり、似た方向を指したりします。

関係
補強しあう SRP と SoC、DIP と OCP、LoD と Tell Don’t Ask
別の側面を見る DRY と Rule of Three、KISSYAGNI
同じ目標を別の言い方で述べる POLA と設定より規約
衝突しうる OCP と KISSDRY と SoC、継承より合成LSP

原則同士の関係を意識すると、「この場面ではどちらを優先するか」を判断しやすくなります。たとえば、OCPで拡張点を作りすぎると過剰設計になり、KISSと衝突します。重複を消すためにDRYを追求すると、関心の分離が壊れることもあります。

原則が衝突するとき

実務では原則同士が衝突します。代表的なケースを挙げます。

  • DRYと早すぎる抽象化: 似たコードを統合すると、変更理由が違うコードが結合される
  • OCPと過剰設計: すべてを拡張可能にすると、現在の要件に対して複雑になる
  • SRPと過剰分割: 責務を細かく切りすぎると全体の流れが追えなくなる
  • 尋ねるな、命じよとレポート処理: クエリ系では情報を取り出す方が自然
  • POLAと革新: 既存の慣習を踏襲するだけでは新しい改善ができない

原則は鎮静剤ではなく、判断のための語彙です。衝突したときは、コストとリスクを比較し、どちらを優先するか説明できるようにしておくと、後の議論で揉めません。

「今の要件で何を変化から守るか」「想定する変更の頻度はどれくらいか」「失敗時の影響はどこに広がるか」を問うことで、原則の優先順位が決まります。

よくある誤解

プログラミング原則は、いくつかの誤解とともに広まってきました。

  • DRYは「コードを短くする」ことではない: 同じ知識の重複を避けるのが目的で、見た目が似ているだけのコードは別物です。
  • SOLIDは「クラスを増やす」ことではない: SRPは責務の分離であり、ファイルやクラス数の増減は副次的なものです。
  • YAGNIは「設計するな」ではない: 必要な抽象は今作る、今要らない「いつか必要かもしれない」機能は作らない、という意味です。
  • KISSは「短く書け」ではない: 説明と修正のしやすさが本質で、ワンライナーは必ずしもKISSではありません。
  • OCPは「クラスを変更してはいけない」ではない: 振る舞いの追加で既存コードを壊さない構造を目指すという指針で、リファクタリングを禁じるものではありません。
  • LSPは「継承を使え」ではない: 継承を選ぶ場合の制約であり、まず継承を使うべきという意味ではありません。

原則の言葉だけを覚えると、こうした誤解を生みやすくなります。原典の文脈や、なぜその原則が必要だったかを合わせて理解すると、誤用を避けられます。

現代開発への接続

オブジェクト指向の文脈で生まれた原則の多くは、関数型、マイクロサービス、クラウド、AIエージェント設計でも形を変えて生きています。

領域 関連する原則
関数型プログラミング SRP(純粋関数)、SoC(副作用の隔離)、DIP(高階関数)
マイクロサービス SRP(サービス分割)、ISP(API分離)、SSOT(マスターデータ)
API設計 POLA、設定より規約ISP
データパイプライン SoC、早く失敗せよ、SSOT
コードとしてのインフラ DRY、SSOT、設定より規約
LLMエージェント SRP(ツールの責務)、KISSYAGNI
テスト設計 早く失敗せよ、SRP、DIP(モック化)

原則そのものは古くからあるものですが、現代の文脈に翻訳することで、新しい技術スタックの設計判断にも使えます。

原則の歴史的タイムライン

主要な原則の発表年を一覧にすると、ソフトウェア工学の進化が見えます。

原則 提唱者
1972 情報隠蔽 David Parnas
1974 関心の分離 Edsger Dijkstra
1974 早すぎる最適化 Donald Knuth
1979 構造化設計(凝集度/結合度) Yourdon, Constantine
1980 ロバストネス原則 Jon Postel
1987 リスコフの置換原則 Barbara Liskov
1988 開放閉鎖原則 Bertrand Meyer
1989 デメテルの法則 Lieberherr, Holland
1991 悪い方が良い Richard Gabriel
1995 継承より合成 GoF
1995 尋ねるな、命じよ Alec Sharp
1997 GRASP Craig Larman
1999 DRY、達人プログラマー Hunt, Thomas
1999 YAGNI(XP) Kent Beck
1999 3度目の法則 Don Roberts via Fowler
2000 SOLID(命名は2004年) Robert C. Martin
2004 設定より規約 David Heinemeier Hansson
2004 早く失敗せよ Jim Shore
2010 ボーイスカウト・ルール Robert C. Martin

これらの多くは独立に提唱されましたが、相互に補強し合って現代の設計慣習を形作っています。

言語別の原則の現れ方

言語ごとに、原則の表現の仕方や強さが変わります。

言語 原則の現れ方
Python EAFP、ダックタイピング、Pythonの禅(PEP 20)
Ruby POLS(最小驚きの原則)、Matz の哲学、メタプログラミング
Java SOLID、デザインパターン、IoC コンテナ、ビーンズ
C++ RAII(資源取得は初期化)、3/5/0の法則
Rust 所有権、借用チェッカー、恐れなき並行性、ゼロコスト抽象
Go 合成、インターフェースを受け取り構造体を返す、小さな重複は小さな依存よりよい
Haskell 純粋関数、型クラス、遅延評価、等式推論
Lisp データとしてのコード、マクロ、REPL駆動
Erlang 壊れたら落とす、アクターモデル、共有なし
TypeScript 型レベル設計、構造的部分型、判別可能なユニオン

各言語の原則を学ぶと、SOLIDDRY が文化的に異なる形で実現されていることが見えてきます。

ケーススタディ: 単一責任原則の適用

単一責任原則の具体的な適用例を考えます。

変更前:

class User:
    def __init__(self, name, email):
        self.name = name
        self.email = email

    def save_to_db(self):
        connection = sqlite3.connect("users.db")
        connection.execute("INSERT INTO users VALUES (?, ?)", (self.name, self.email))
        connection.commit()

    def send_welcome_email(self):
        smtp = smtplib.SMTP("smtp.example.com")
        smtp.sendmail("noreply@example.com", self.email,
                      f"Welcome, {self.name}!")

このクラスは三つの責務を持っています。

  • ユーザーのデータを保持
  • データベースへの保存
  • メール送信

変更理由が三つある(DB変更、メール変更、ユーザー属性変更)ので、SRP違反です。

変更後:

@dataclass
class User:
    name: str
    email: str

class UserRepository:
    def save(self, user: User):
        connection = sqlite3.connect("users.db")
        connection.execute("INSERT INTO users VALUES (?, ?)",
                            (user.name, user.email))
        connection.commit()

class WelcomeEmailService:
    def __init__(self, smtp_client):
        self.smtp = smtp_client

    def send(self, user: User):
        self.smtp.sendmail("noreply@example.com", user.email,
                            f"Welcome, {user.name}!")

責務が分離され、それぞれがテストしやすく、変更しやすくなりました。

ケーススタディ: 開放閉鎖原則の適用

開放閉鎖原則の例。新しい支払い方法を追加するときに、既存コードを変更せず拡張だけで済ませる設計です。

変更前:

def process_payment(method, amount):
    if method == "credit_card":
        # クレジットカード処理
        return charge_credit_card(amount)
    elif method == "paypal":
        # PayPal処理
        return charge_paypal(amount)
    elif method == "bank_transfer":
        return charge_bank(amount)

新しい支払い方法を追加するたびに process_payment を編集する必要があります。

変更後:

class PaymentMethod:
    def charge(self, amount: int) -> Receipt: ...

class CreditCardPayment(PaymentMethod):
    def charge(self, amount): ...

class PayPalPayment(PaymentMethod):
    def charge(self, amount): ...

class BankTransferPayment(PaymentMethod):
    def charge(self, amount): ...

def process_payment(method: PaymentMethod, amount: int) -> Receipt:
    return method.charge(amount)

新しい支払い方法は PaymentMethod を継承する新しいクラスを追加するだけで、process_payment は変更不要です。

ケーススタディ: 依存関係逆転の原則の適用

依存関係逆転の原則の例です。

変更前:

class EmailNotifier:
    def send(self, msg): ...

class OrderService:
    def __init__(self):
        self.notifier = EmailNotifier()  # 具体に直接依存

    def place_order(self, order):
        # 注文処理
        self.notifier.send(f"Order placed: {order.id}")

OrderServiceEmailNotifier の具体実装に直接依存しています。テスト時にメール送信をモック化できないし、SMSやSlack通知に切り替えられません。

変更後:

class Notifier(Protocol):
    def send(self, msg: str): ...

class EmailNotifier:
    def send(self, msg): ...

class SlackNotifier:
    def send(self, msg): ...

class OrderService:
    def __init__(self, notifier: Notifier):  # 抽象に依存
        self.notifier = notifier

    def place_order(self, order):
        self.notifier.send(f"Order placed: {order.id}")

OrderServiceNotifier 抽象に依存し、具体実装は外部から注入されます。テストでは MockNotifier、本番では EmailNotifier を使えます。

よくある質問

Q. オブジェクト指向設計の五原則をすべて毎回守る必要があるか?

A. いいえ。SOLID は判断軸であり、教義ではありません。スクリプトや小規模ツールでは過剰になります。クラスや関数が変更を受け始めて、変更理由が複数発生してから適用すると効果的です。

Q. 知識の重複を避ける原則と3度目の法則は矛盾しないか?

A. 矛盾しません。DRY は「知識の重複を避けよ」という最終目標、Rule of Three は「いつ抽象化するか」のタイミングです。最初の重複は容認し、3度目に共通化を検討する、という穏当な指針として共存します。

Q. 単純に保つ原則とオブジェクト指向設計の五原則は両立するか?

A. 両立します。SOLID は本質的な複雑さを管理する技法、KISS は偶発的な複雑さを抑える指針です。「SOLID を理由に過剰設計をする」は、SOLIDKISS も理解していない兆候です。

Q. 今必要ないものは作らない原則と長期計画は矛盾しないか?

A. 一見矛盾しますが、YAGNI は「実装するな」であり「考えるな」ではありません。アーキテクチャの方向性、データモデルの基本構造などは事前に考えるべきです。実装は具体的な要件が来てから、というのが YAGNI の意味です。

Q. 継承より合成は継承禁止と同じ意味か?

A. いいえ。継承が「である」関係で本当に成立し、LSPを満たすなら、継承は短く強力です。問題は「再利用のための継承」であり、概念的な包含関係を伴わない継承の濫用です。

Q. 尋ねるな、命じよとクエリは矛盾しないか?

A. レポートやクエリ系は Ask が自然で、原則の対象外です。Tell Don’t Ask は、ドメインの判断を呼び出し側に漏らさない、という意味で使うのが適切です。

Q. 設定より規約はカスタマイズ性を犠牲にしないか?

A. する場合があります。規約と異なる要件が出ると、規約を上書きするコストが高くなることがあります。利益(生産性)とコスト(柔軟性)のトレードオフを意識する必要があります。

Q. 早く失敗せよと利用者向けエラーは両立するか?

A. 内部処理では早く失敗し、利用者インターフェースでは検証してから親切なメッセージを返す、という階層的な使い分けが現実的です。境界の場所で例外を捕捉し、形式を整える層を挟みます。

Q. ボーイスカウト・ルールでプルリクエストが肥大化する場合は?

A. 関連する小さな改善はOK、無関係な改善は別 PR、というルールをチームで決めると安全です。レビュアーが追えなくなる規模になったら、別途リファクタリング PR を立てます。

Q. 最小驚きの原則と既存の慣習を破る変更は両立するか?

A. 破ってもよいが、理由とドキュメントが必要です。「より良い慣習を提案するため」に POLA を破るなら、ドキュメントとサンプルで学習コストを下げる責任があります。

Q. 単一責任原則は関数にも適用できるか?

A. はい。関数は「一つの目的を果たす」べきで、複数の処理を行う関数は分割を検討します。長すぎるメソッドというコードスメルは、関数レベルのSRP違反と解釈できます。

Q. 原則の対立を解消する一般的なルールはあるか?

A. ありません。状況依存です。「間違ったときのコストはいくらか」と「何が変化しやすいか」を問うと、優先する原則を選びやすくなります。

原則を実践に落とし込むためのチェックリスト

設計レビューやコードレビューで使えるチェックリストです。

  • このクラス/関数の変更理由を一文で説明できるか
  • 拡張するとき、既存コードを変えずに済むか
  • 派生型は基底型と置換可能か
  • 利用者は不要なメソッドに依存させられていないか
  • 依存は抽象に向いているか、具体に向いているか
  • 同じ知識が複数箇所に書かれていないか
  • いま実装しようとしているのは「今必要」か「将来かも」か
  • メソッドチェーンの中で内部構造を覗いていないか
  • データを取り出して判断するか、依頼しているか
  • 利用者の予想を裏切る挙動はないか
  • 失敗が遅い場所まで伝播していないか
  • 触れたついでに少し綺麗にできる場所はないか

すべてに「はい」と答えられる必要はありません。一つでも引っかかったら、改善の機会として議論するのが、原則の実用的な使い方です。

原則を学ぶための原典の読み方

原則を深く理解するには、原典に近い文献を読むことが重要です。

  • 一次資料: Robert C. Martin, Bertrand Meyer, Barbara Liskov, David Parnas, Edsger Dijkstra の論文や著書
  • 二次資料: Refactoring Guru、Martin Fowler のブログ記事
  • 教科書: 「クリーンコード」「達人プログラマー」「リファクタリング」「コードコンプリート」
  • 実装例: GitHub のオープンソースプロジェクトを SOLID キーワードで検索

二次資料は分かりやすいですが、しばしば誤解や単純化が混じります。原典に立ち返って意図を確認すると、原則をより正確に理解できます。

各言語のドキュメントから読む原則

主要言語の公式ドキュメントには、それぞれの言語が暗黙に前提とする原則が書かれています。

Python の禅(PEP 20):

import this
# 美しいものは醜いものよりよい。
# 明示的であることは暗黙的であることよりよい。
# 単純なものは複雑なものよりよい。
# 複雑なものは入り組んだものよりよい。
# 平坦なものは入れ子よりよい。
# 疎なものは密なものよりよい。
# 読みやすさは大切である。
# 特別な場合であっても、規則を破るほど特別ではない。
# ただし、実用性は純粋性に勝る。
# エラーを黙って見過ごしてはならない。
# 明示的に黙らせる場合を除く。
# 曖昧さに直面したら、推測したくなる誘惑を拒め。
# やり方は一つ、できれば一つだけであるべきだ。
# ただし、そのやり方は最初は明らかでないかもしれない。
# 今やる方が、まったくやらないよりよい。
# ただし、今すぐやらない方がよいことも多い。
# 実装を説明しにくいなら、それは悪いアイデアである。
# 実装を説明しやすいなら、それは良いアイデアかもしれない。
# 名前空間はすばらしいアイデアだ。もっと活用しよう。

Go のことわざ(Rob Pike)から抜粋:

メモリ共有で通信するな。通信によってメモリを共有せよ。
並行性は並列性ではない。
チャネルは調停し、ミューテックスは直列化する。
インターフェースが大きいほど、抽象は弱くなる。
ゼロ値を役に立つものにせよ。
空インターフェースは何も語らない。
gofmtのスタイルは誰のお気に入りでもないが、gofmtは誰のお気に入りでもある。
少しのコピーは、少しの依存よりよい。
システムコールは常にビルドタグで守れ。
cgoは常にビルドタグで守れ。
cgoはGoではない。
unsafeパッケージに保証はない。
明快さは巧妙さに勝る。
リフレクションは決して明快ではない。
エラーは値である。
エラーを確認するだけでなく、丁寧に扱え。
アーキテクチャを設計し、構成要素に名前を付け、詳細を文書化せよ。
ドキュメントは利用者のためにある。
panicするな。

Erlang/OTP の哲学:

壊れたら落とせ。
まず動かし、正しくし、それから速くせよ。
耐障害性を組み込む。
実行中のコード更新。
並行性は機能である。

Rust の哲学:

誰もが信頼性と効率の高いソフトウェアを作れるようにする。
ガベージコレクションなしのメモリ安全性。
データ競合のない並行性。
オーバーヘッドのない抽象。
停滞のない安定性。

各言語の文化を読むと、SOLIDDRYのような原則がどう翻訳されているかが見えてきます。

巨大コードベースでの原則の役割

100万行を超える巨大コードベース(カーネル、ブラウザ、ERP)では、原則の役割が大きく変わります。

  • DRY: 巨大コードベースでは、安易な共通化が依存を爆発させる。重複の方が安全な場合が多い
  • SOLID: モジュール境界の設計に直結する。境界を間違えると組織が破綻する
  • ISP: 巨大インターフェース(Java の java.util.Collection のような)を超えると変更が困難
  • DIP: 横断的関心事をDIで管理しないと、テスト不可能なコードベースになる
  • KISS: シンプルさは長期保守性に直結する
  • POLA: 数千人の開発者が触るコードでは予測可能性が必須
  • 設定より規約: 規約がないと、無数のスタイルが混ざる

「Googleのソフトウェアエンジニアリング」という書籍は、Googleが巨大コードベース(モノリポ)で原則をどう実践しているかを詳述しています。ハイラムの法則の命名者である Hyrum Wright も執筆者の一人です。

関数型プログラミングと原則

関数型プログラミング(FP)の原則は、OOPのSOLIDと一見違うように見えますが、本質は重なります。

  • 純粋関数 ≒ SRP(副作用がない関数は一つのことをする)
  • 不変データ ≒ DIP(状態の変更を抽象に閉じ込める)
  • 高階関数 ≒ OCP(関数を渡すことで振る舞いを拡張)
  • 型クラス・トレイト ≒ ISP(必要な能力だけ要求)
  • 副作用の隔離 ≒ SoC(純粋部分と副作用部分の分離)

Haskell、OCaml、F#、Scala、Clojure では、これらが言語機能として強制されます。Python、JavaScript、Ruby のようなマルチパラダイム言語では、関数型の原則を意識的に適用することで設計を強化できます。

マイクロサービスと原則

マイクロサービスはSOLIDをサービス境界に拡張した形と読めます。

  • SRP → サービスは一つのビジネス機能を担当
  • OCP → 新サービスを追加し、既存サービスを変更しない
  • LSP → サービス間契約(API)の互換性を守る
  • ISP → 細かいエンドポイントごとにスコープを切る
  • DIP → サービス同士は抽象(API契約)に依存

ただし、マイクロサービスにはネットワーク、認証、再試行、整合性、観測性という追加の複雑さがあります。原則を適用しつつ、運用コストとのバランスを考える必要があります。

「ドメイン駆動設計」(Eric Evans, 2003)の境界づけられたコンテキストは、マイクロサービスの境界設計の基礎理論として広く参照されます。

AIエージェント設計と原則

LLMを使ったエージェント設計でも、古典的な原則は生きています。

  • SRP: ツールは一つの目的に集中(search_documentssummarize_document を分ける)
  • ISP: エージェントには必要なツールだけ提供
  • DIP: モデルとビジネスロジックを分離
  • KISS: プロンプトはシンプルに、複雑さは外部関数に
  • YAGNI: ツールチェーンを増やしすぎない
  • 早く失敗せよ: 不正な出力は早期に検出して再実行
  • POLA: ユーザーが期待するスタイルで応答

エージェントは新しいパラダイムですが、設計判断のコアは変わりません。

原則を学ぶための演習

設計力を磨くには、原則を実例に当てはめて考える練習が効きます。

演習1: 既存のオープンソースコードからSOLIDの実例を見つける

  • Spring Framework の依存性注入 (DIP)
  • Django の Form クラス (SRP)
  • React Hooks(継承より合成)
  • Linux カーネルのデバイスドライバ (OCP)

演習2: 自分のコードベースを5つの軸でレビュー

  • どのクラスが God Object になりつつあるか
  • どこにSpaghettiの臭いがあるか
  • どこが投機的一般化か
  • どこがハイラムの法則に晒されているか
  • どこが設定より規約の恩恵を受けているか

演習3: 原則を破った設計を意図的に作って、何が起きるか観察する

  • SRPを破ったクラスをテストしてみる
  • DIPを無視して具象に依存するコードを書いてみる
  • DRYを過剰に適用して、変更時に苦しむ
  • YAGNIを無視して将来機能を作り、ROIを観察する

これらは座学では得られない理解を与えます。

原則の進化

原則も時代とともに進化します。

時代 主な原則の中心
1970年代 構造化プログラミング、凝集度/結合度、SoC
1980年代 OOP、情報隠蔽、開放閉鎖
1990年代 デザインパターン、SOLID継承より合成
2000年代 実践重視、DRYYAGNI、TDD、XP
2010年代 クリーンコード、関数型、合成、Twelve-Factor
2020年代 クラウドネイティブ、オブザーバビリティ、ドメイン駆動、ハイラムの法則

新しい時代の原則は、古い原則を否定するのではなく、新しい文脈に翻訳して引き継いでいます。

原則と組織文化

原則は技術的な指針であると同時に、組織文化の一部でもあります。

  • レビュー文化: 原則を共有する語彙として使う
  • 採用面接: 候補者の設計感覚を見るための質問
  • メンタリング: 新人に原則を教えながら判断軸を伝える
  • ADR: 原則に基づく意思決定を残す
  • 設計レビュー: チームで原則を議論しながら設計を磨く

原則を組織文化に根付かせるには、時間がかかります。「コードレビューで毎回 SOLID を引用する」のではなく、「設計判断に迷ったときに原則名で議論できる」状態を目指すと、原則は無理なく浸透します。

原則を批判的に見る

原則は便利ですが、批判的視点も必要です。

  • 原則は「一般化された経験」であり、状況依存性がある
  • 原則同士は矛盾することがあり、一律適用できない
  • 原則を守ることが目的化すると、本来の目的を見失う
  • 原則名を引用するだけで設計が良くなるわけではない
  • 原則を破る勇気も時に必要(破る理由を説明できるなら)

特に SOLID は、関数型プログラマーから「OOPの欠陥を補うための後付け」と批判されることもあります。Rich Hickey(Clojure 作者)の「単純さは簡単さとは違う」講演は、「単純」と「簡単」を区別する視点で、KISSSOLIDを再評価する内容です。

原則は道具です。道具を使うのは判断力を持った人間です。

付録A: 原則早見表

原則 短い言葉 提唱者
SRP クラスの変更理由は一つに Robert C. Martin 2000
OCP 拡張に開き、変更に閉じる Bertrand Meyer 1988
LSP 派生型は基底型と置換可能 Barbara Liskov 1987
ISP 不要なインターフェースに依存させない Robert C. Martin 2000
DIP 抽象に依存し、具体に依存しない Robert C. Martin 2000
DRY 同じ知識を二箇所に書くな Hunt, Thomas 1999
KISS 単純に保て Kelly Johnson 1960
YAGNI 今いらないものは作るな Kent Beck 1999
LoD 直接の友達としか話すな Lieberherr 1989
継承より合成 継承より合成 GoF 1995
尋ねるな、命じよ データを取り出して判断するな、依頼せよ Alec Sharp 1995
SoC 関心を分離せよ Edsger Dijkstra 1974
SSOT 真実は一箇所にだけ - -
POLA 利用者を驚かせるな - 1972頃
設定より規約 規約があれば設定不要 David Heinemeier Hansson 2004
早く失敗せよ 早く失敗せよ Jim Shore 2004
ボーイスカウト・ルール 触れたコードは少し綺麗にして去る Robert C. Martin 2010
3度目の法則 3度目で初めて抽象化 Don Roberts 1996
IoC / ハリウッド原則 こちらから呼び出す Michael Jackson 1980年代
GRASP 責務をどう割り当てるか Craig Larman 1997
情報隠蔽 設計判断を隠せ David Parnas 1972
悪い方が良い 単純さは正しさに勝る場合がある Richard Gabriel 1991
Robustness 厳密に出し、寛容に受ける Jon Postel 1980
銀の弾丸はない 銀の弾丸はない Fred Brooks 1986

付録B: 原則の対立マップ

原則同士は補完だけでなく対立もします。代表的な対立を整理します。

対立 解決の指針
DRY3度目の法則 知識の重複は避ける、見た目の重複は3度待つ
OCP とKISS 変更されそうな軸だけ拡張点を作る
SRP と全体可読性 過剰分割を避け、責務単位の粒度を選ぶ
尋ねるな、命じよ とレポート処理 クエリ系は問い合わせ、ドメイン系は依頼
POLA と革新 慣習を破るならドキュメントとサンプルで補う
合成と継承 is-a が真なら継承、再利用なら合成
ポステルの法則と早く失敗せよ セキュリティ重要なら早く失敗し、互換性重視ならポステルの法則
規約と柔軟性 多くの場合は規約で足りるが、上書きの道を残す
YAGNI と先を見た計画 実装はYAGNI、設計は先を見る
DRY とSoC 知識が同じならDRY、関心が違うならSoCで分離

付録C: 設計レビューでの問い

設計レビューで原則を使った質問のテンプレートです。

  • このクラスを変更する理由は何種類あるか(SRP)
  • 新しいケースを追加するとき、既存コードに何箇所変更が要るか(OCP)
  • このサブクラスは基底クラスと置換可能か(LSP)
  • このインターフェースに依存するクライアントは、すべてのメソッドを使うか(ISP)
  • このモジュールは抽象に依存しているか、具象に依存しているか(DIP)
  • 同じビジネスルールが複数箇所に書かれていないか(DRY)
  • このコードはあとから読んでも意図を取れるか(KISS)
  • この拡張点は今必要か、それとも将来かもしれないか(YAGNI)
  • このメソッドチェーンは内部構造を覗いていないか(LoD)
  • データを取り出して判断する代わりに、依頼できないか(尋ねるな、命じよ)
  • この値は一箇所だけにあるか、複数箇所で同期しているか(SSOT)
  • 利用者は予想外の挙動に驚かないか(POLA)
  • 失敗が起きたとき、すぐに原因が分かるか(早く失敗せよ)
  • 触れたついでに改善できる小さな箇所はないか(ボーイスカウト・ルール)

これらの問いを習慣にすることで、原則が自然に設計に染み込みます。

付録D: 学習のためのおすすめ書籍

  • Robert C. Martin, 「クリーンコード」(クリーンコード入門)
  • Robert C. Martin, 「クリーンアーキテクチャ」(アーキテクチャ視点のSOLID)
  • Andy Hunt, Dave Thomas, 「達人プログラマー」(DRYの原典)
  • Martin Fowler, 「リファクタリング」(コードスメルとリファクタリング)
  • Erich Gamma et al., 「デザインパターン」(GoF、継承より合成の原典)
  • Bertrand Meyer, 「オブジェクト指向ソフトウェア構築」(OCPの原典)
  • David Parnas et al., 「ソフトウェアの基礎」(情報隠蔽の論文集)
  • Edsger Dijkstra, 「プログラミングの規律」(SoCを含む基礎論文)
  • Eric Evans, 「ドメイン駆動設計」(ドメイン境界の設計)
  • Titus Winters et al., 「Googleのソフトウェアエンジニアリング」(実践的なハイラムの法則など)
  • Craig Larman, 「UMLとパターンの適用」(GRASPの原典)
  • Kent Beck, 「エクストリームプログラミング入門」(YAGNIの原典)

これらを読むと、原則の文脈と背景が深く理解できます。

付録E: 各原則の深掘り

ここでは、特に重要な原則について、実例を使ってさらに深く掘り下げます。

SRP の深掘り

SRP(Single Responsibility Principle)は最もよく引用される原則ですが、誤解も多いです。Robert C. Martin は2017年の Clean Architecture でこう述べています。

The principle is about people. The actor that requests a change is the source of the responsibility.

意訳「これは人々に関する原則だ。変更を要求するアクター(人や役割)こそが責務の源である」。

つまり、SRP は「機能の数」ではなく、「変更を要求するステークホルダー」を基準に責務を分けるべき、という指針です。

例として、給与計算システムを考えます。

# 悪い例
class Employee:
    def calculate_pay(self): ...     # 経理部の関心
    def report_hours(self): ...      # 人事部の関心
    def save(self): ...              # DBA の関心

このクラスは3つの異なる部署(アクター)からの変更要求にさらされます。経理部の規則変更で、人事部の機能が壊れる可能性があります。

# 改善
class PayCalculator:    # 経理部
    def calculate(self, employee): ...

class HourReporter:     # 人事部
    def report(self, employee): ...

class EmployeeRepository:  # DBA
    def save(self, employee): ...

それぞれが一つのアクターだけに対応します。

OCP の深掘り

OCP(Open/Closed Principle)の本質は、変更点を予想して拡張点を作る ことです。Bertrand Meyer はこれを Eiffel 言語の設計理念として提唱しました。

ただし、すべての場所をOCPで拡張可能にすると過剰設計になります。Robert Martin は Strategic Closure という概念で説明しています。

You can’t be 100% closed. You have to choose what kinds of changes to be closed against.

意訳「100%閉じることはできない。どの種類の変更に対して閉じるかを選ぶ必要がある」。

実例として、決済システムで考えます。

  • 支払い方法(クレジットカード、PayPal、銀行振込)は新規追加が頻繁 → ここに拡張点を置く
  • 通貨計算ロジックは変わらない → ここは閉じてよい
  • 為替レートの取得方法は変わる可能性 → ここに拡張点を置く

すべての変更を予想して全面的に開くと、Speculative Generality になります。変更されそうな軸 を見極めることが OCP の実践です。

LSP の深掘り

LSP(Liskov Substitution Principle)は、Barbara Liskov が1987年の OOPSLA 講演で提示しました。1994年には Liskov と Jeannette Wing の共著論文で形式化されました。

形式的には、型 S が型 T の派生型であるとは、T を期待する文脈で S を使っても、プログラムの正しさが保たれることを言います。

具体的な要件:

  • 事前条件は派生型で強められない
  • 事後条件は派生型で弱められない
  • 不変条件は派生型でも保たれる
  • 例外は派生型で増えてはいけない

例として、Square と Rectangle の問題を考えます。

class Rectangle:
    def __init__(self, w, h):
        self._w = w
        self._h = h

    def set_width(self, w):
        self._w = w

    def set_height(self, h):
        self._h = h

    def area(self):
        return self._w * self._h

class Square(Rectangle):
    def set_width(self, w):
        self._w = w
        self._h = w  # 高さも変わる!

    def set_height(self, h):
        self._w = h
        self._h = h

これは LSP 違反です。Rectangle を期待するクライアントが Square を渡されると、set_widthset_height も変えるという予想外の挙動に出会います。

def stretch(rect: Rectangle):
    rect.set_width(10)
    rect.set_height(5)
    assert rect.area() == 50  # Square なら 25 になる

解決策は継承関係を見直すことです。Square is-a Rectangle ではなく、SquareRectangle は別の Shape の派生にする、あるいは可変メソッドを取り除いて不変オブジェクトにする、などです。

ISP の深掘り

ISP(Interface Segregation Principle)は、Java のような「事前にインターフェースを宣言する言語」で特に重要です。Robert Martin は XeroxSTAR プリンタソフトウェアの実例で説明しています。

巨大なインターフェース Job に多くのメソッドが定義され、すべての利用クラスがすべてのメソッドに依存していました。一つのメソッド変更でシステム全体の再コンパイルが必要でした。

ISP に従って、JobPrintJobStaplingJobPunchingJob などに分割すると、変更の影響が限定されます。

Go の interface は ISP の理想形です。利用側が 必要なメソッドだけ を持つインターフェースをローカルに宣言できます。

// 利用側で定義
type Reader interface {
    Read([]byte) (int, error)
}

func process(r Reader) { ... }

// io.File は Read を持つので使える、Write などは要求されない

DIP の深掘り

DIP(Dependency Inversion Principle)は 依存性の方向を逆転させる ことです。直感的には、ビジネスロジック層がデータベース層に依存する 自然な向き を、データベース層がビジネスロジック層の抽象に依存する 逆の向き に変えます。

flowchart TB subgraph Before["DIP違反"] A1["UserService"] --> B1["PostgresDB"] end subgraph After["DIP遵守"] A2["UserService"] --> I["UserRepository (抽象)"] B2["PostgresDB"] --> I end

Clean ArchitectureHexagonal ArchitectureOnion Architecture はすべて DIP を中心とした設計手法です。共通する考え方は、内側のドメインロジックが外側のインフラに依存しない、ということです。

実装としては DI コンテナ(Spring、Guice、ASP.NET Core DI)が代表的ですが、関数型言語では関数を引数に渡す形(高階関数)も DIP の実現手段です。

DRY の深掘り

DRY の本質は Single Source of Truth です。ただし、何を Single にするかを誤ると、結合度が増します。

# 共通化しすぎた例
class Person:
    def get_name(self): ...

class User(Person): ...  # ユーザー
class Customer(Person): ...  # 顧客

# Customer に「会社名」が追加されたとき
# User と Customer の Name の意味が違うことが分かる
# 共通の Person を持つ判断が誤りだった

Sandi MetzThe Wrong Abstraction(2014)という有名な記事は、重複は抽象の間違いより安い と論じます。

Duplication is far cheaper than the wrong abstraction.

DRY を守ろうとして抽象を作ったが、後でそれが間違いだと気づいたとき、抽象を解除するコストは元の重複を作るコストより大きい場合があります。

実践的な指針:

  • 偶然似ているだけの2つのコードは、無理に統合しない
  • 同じビジネスルールが3度現れたら、抽象化を検討する(Rule of Three)
  • 抽象を作るときは、その抽象の生命を予想する
  • 抽象が間違っていると分かったら、潔くインライン化する

KISS の深掘り

KISS は「単純さ」を求めますが、Rich Hickey(Clojure 作者)は SimpleEasy を区別する有名な講演 Simple Made Easy(2011)を行いました。

  • Simple: 一つの目的、一つの役割、絡まっていない(un-tangled)
  • Easy: 慣れている、近くにある、すぐ使える

これらは一致しません。例えば:

  • ORM は Easy(書きやすい)だが Simple ではない(複雑な内部)
  • Stored Procedure は Simple(明確な役割)だが Easy ではない(学習コスト)

KISSSimple は、Hickey の意味での Simple、つまり un-tangled(絡まっていない)を指します。一見複雑に見えても、責務が明確で結合が少ないなら、それは Simple です。

逆に、見た目は短くても、副作用や暗黙の依存があるコードは Simple ではありません。

YAGNI の深掘り

YAGNI今いらないものを作るな ですが、設計するな ではありません。Martin Fowler は4つの拡張点を区別すべきと述べています。

  • 構造的な拡張点(モジュール境界、レイヤー): これは事前に設計する
  • 振る舞いの拡張点(プラグイン、ストラテジー): 必要が見えてから作る
  • 設定可能性: 利用者が触る部分は早めに、内部は後でよい
  • 性能上の拡張点(キャッシュ、並列化): 計測してから

The wrong feature を作るコストは、The missing feature を作るコストより大きい場合があります。一度作ったコードは消せず、保守コストを生み続けます。

逆に、本当に必要だと分かった機能は、後から作ってもそれほど追加コストがかかりません。Just-in-time abstraction の方が、Just-in-case abstraction より安全です。

LoD の深掘り

LoD(Law of Demeter)は、Karl Lieberherr が Demeter Project の中で提唱しました。Demeter は Aspect-Oriented Programming の前身として、横断的関心事の分離を研究するプロジェクトでした。

LoD の正確な定式化(Lieberherr 1989):

A method M of an object O may only invoke methods of the following kinds of objects:

  1. O itself
  2. M’s parameters
  3. Any objects created/instantiated within M
  4. O’s direct component objects
  5. A global variable, accessible by O, in the scope of M

実例として、JavaScript のオプショナル連鎖を考えます。

// LoD 違反
const city = order.customer.address.city;

// LoD 遵守
const city = order.getCustomerCity();

order.getCustomerCity() の中身は this.customer.getCity() であり、Customer.getCity()this.address.getCity() です。各レイヤーが自分の隣の友達としか話さないので、構造変更が局所化されます。

ただし、データクラスや DTO に対する連鎖は LoD の対象外と考えるのが現代的です。record.address.city のような単純なデータ参照に LoD を適用すると、コードが冗長になります。

Composition over Inheritance の深掘り

GoF(1995)の Design Patterns では、Favor object composition over class inheritance という原則が示されました。GoF はその根拠を以下のように述べています。

  • 継承は実装を継承するため、ホワイトボックスの再利用
  • 合成は契約を継承するため、ブラックボックスの再利用
  • 継承は静的(コンパイル時)、合成は動的(実行時)に切り替え可能
  • 継承は親の変更が子に波及(Fragile Base Class)

具体例: GUI のボタン。

# 継承で機能を追加
class Button: ...
class IconButton(Button): ...
class IconHoverButton(IconButton): ...
class IconHoverDisabledButton(IconHoverButton): ...
# 組合せが指数的に爆発
# 合成で機能を追加
class Button:
    def __init__(self, decorators):
        self.decorators = decorators

# Decorator パターンで動的に組合せ
button = Button([IconDecorator(), HoverDecorator(), DisabledDecorator()])

合成は次のような場合に特に効果的です。

  • 機能の組み合わせが多い
  • 動的に振る舞いを切り替えたい
  • フレームワーク作者ではなく利用者
  • 部分的な再利用がしたい

ただし、継承は次の場合に短く強力です。

  • is-a 関係が真
  • 振る舞いの大部分を共有
  • LSP を完全に満たす
  • 階層が浅い(3段以下)

Tell Don’t Ask の深掘り

Tell Don’t Ask は、データを取り出して呼び出し側で判断する パターンを データを持つオブジェクトに依頼する パターンに変える指針です。

Pragmatic Programmer の例:

# Ask
if account.balance >= amount
  account.balance -= amount
end

# Tell
account.withdraw(amount)

Ask の問題:

  • ロジックが呼び出し側に分散する
  • 並行アクセスで race condition
  • カプセル化が破られる
  • 変更が複数箇所に波及

Tell の利点:

  • ドメインロジックがオブジェクト内に集約
  • 不変条件を守れる
  • 並行性を内部で扱える
  • カプセル化が保たれる

ただし、すべての場面で Tell が正解ではありません。レポート、クエリ、不変オブジェクトに対する Read 操作は Ask が自然です。

CQS(Command Query Separation、Bertrand Meyer)の考え方と組み合わせると、Command(Tell)と Query(Ask)を明確に分けられます。

SoC の深掘り

SoC(Separation of Concerns)は Edsger Dijkstra が1974年に提唱しましたが、その背景には Dijkstra の Goto Considered Harmful(1968)からの一連の構造化プログラミング論があります。

Dijkstra は Notes on Structured Programming(1972)で、人間の認知能力には限界があり、複雑なシステムを理解するためには関心を分離する必要がある、と論じました。

現代のソフトウェアでの SoC の現れ:

  • レイヤード・アーキテクチャ(プレゼンテーション、ドメイン、データアクセス)
  • MVC、MVVM、MVP
  • クライアントとサーバ
  • フロントエンドとバックエンド
  • ビジネスロジックとインフラ
  • 認証と認可
  • ログとビジネス処理

SoC は SRP と似ていますが、粒度が違います。SRP はクラス単位、SoC はアーキテクチャ全体に適用されます。

POLA の深掘り

POLA(Principle of Least Astonishment)は、利用者の メンタルモデル を尊重する原則です。

メンタルモデルとは、このシステムはこう動くはずだ という利用者の予想です。POLA は、このメンタルモデルから外れる挙動を避けるべき、と説きます。

実例:

  • Python の + が文字列で連結に使えるが、文字列と数値では失敗する → 一貫性
  • Java の String == String が値比較ではなく参照比較 → 驚き
  • JavaScript の [] + {} == "[object Object]" → 驚き
  • Ruby の nil.to_s == "" → 驚きを許容(便利)

POLA の本質は、API設計者は利用者の経験を予想すべき という姿勢です。これは Convention over Configuration の根底にもあります。

ただし、POLA は文化に依存します。Ruby の nil.to_s は Rubyist には自然、Pythonist には驚き。原則を守るには 誰の メンタルモデルかを意識する必要があります。

Convention over Configuration の深掘り

Convention over Configuration(CoC)は、David Heinemeier Hansson が Ruby on Rails で広めましたが、概念自体は古く、Maven のような Java ビルドツールでも採用されていました。

CoC の核心は 決定の数を減らす ことです。

# Rails の規約
# モデル User → テーブル users
# コントローラ UsersController → /app/controllers/users_controller.rb
# ビュー → /app/views/users/index.html.erb
# テスト → /spec/models/user_spec.rb

利用者は規約に従えば動くものが作れます。設定を書く必要があるのは、規約から外れる場合だけです。

CoC は Decision Fatigue(決定疲れ)を減らす効果があります。一日に下せる質の高い決定は限られているため、本質的な決定 に集中できる環境が生産性を上げます。

ただし、規約から大きく外れる要件には CoC が逆風になります。規約を曲げる のと 規約に従う の境界が、CoC の使い所です。

Fail Fast の深掘り

Fail Fast は、Jim Shore が IEEE Software で提唱しましたが、概念自体は分散システムや並行プログラミングで以前から使われていました。

Fail Fast の本質は エラーが発生したら、すぐに止めて、原因が分かる場所で報告する ことです。

実装パターン:

  • 引数チェック: メソッドの先頭で
  • 不変条件チェック: コンストラクタや状態遷移で
  • 環境チェック: 起動時の設定検証
  • アサーション: 開発時の前提条件確認
  • タイムアウト: 無期限待ちを防ぐ
def transfer(amount, from_account, to_account):
    # Fail Fast
    assert amount > 0, "amount must be positive"
    assert from_account is not None
    assert to_account is not None
    assert from_account.id != to_account.id

    # 本処理
    from_account.withdraw(amount)
    to_account.deposit(amount)

Fail Fast の対極は Fail Silent(黙って失敗する)です。例外を握りつぶす、null を返す、デフォルト値を入れる、などです。

これらは短期的には便利ですが、長期的にはバグを見えなくします。Robert Martin は Clean Code で、例外を吸い込むのではなく、適切なレベルで処理する ことを推奨しています。

Boy Scout Rule の深掘り

Boy Scout Rule の元になったボーイスカウトの精神は、Robert Baden-Powell が1908年の Scouting for Boys で示した次の言葉です。

Try and leave this world a little better than you found it.

これは個人の倫理観ですが、Robert Martin が 97 Things Every Programmer Should Know(2010)でソフトウェア開発に適用しました。

実践のコツ:

  • 触れたファイルで一つだけ改善
  • レビュアーの注意を分散させない
  • 改善内容をコミットメッセージで明示
  • セキュリティリスクのある変更は別 PR

Boy Scout Rule は Continuous Refactoring の文化を作る基礎です。一度に大規模な書き直しではなく、日常の中で少しずつ改善することで、技術的負債の蓄積を防ぎます。

ただし、触ったついでに大幅に書き換える のは、Boy Scout Rule の意図ではありません。レビューしやすい範囲、無関係な機能を壊さない範囲、で改善するのがマナーです。

Rule of Three の深掘り

Rule of Three は、Don Roberts が Martin Fowler に伝え、Refactoring で広まりました。

The first time you do something, you just do it. The second time you do something similar, you wince at the duplication, but you do the duplicate thing anyway. The third time you do something similar, you refactor.

意訳「最初は単にやる。2度目は重複に気付くが、それでも重複させる。3度目に似たことをするときに、リファクタリングする」。

Sandi Metz の The Wrong Abstraction と組み合わせると、Rule of Three の慎重さが理解できます。最初の2例だけでは、何が共通すべきで、何が独自であるべきかが分かりません。3例見ることで、ようやく 共通の本質 が見えてきます。

実践的には:

  • 1度目: 単純に書く
  • 2度目: 似ているコードに気付く、コメントで関連を示す
  • 3度目: 共通点を抽出する

抽象化のタイミングを早めると Speculative Generality、遅らせすぎると Duplicated Code、というバランスを取るための指針です。

まとめ

プログラミング原則は、設計判断のときに参照する語彙の集合です。SOLIDは責務と依存の方向、DRYとSSOTは知識の唯一性、KISSYAGNIは過剰設計の抑制、POLAと設定より規約は予測可能性、早く失敗せよボーイスカウト・ルールは検出と継続的改善を扱います。

それぞれの原則は単独では成立せず、衝突や補強を通じて互いを意味づけます。重要なのは、原則名を暗記することではなく、コードを書きながら「今ここでは何を変化から守るのか」「何を予測可能に保つのか」を問えるようになることです。

原則を一つのレンズとして使えると、コードレビューの会話は短くなり、設計判断は説明可能になります。デザインパターンが「具体的な構造の語彙」だとすれば、プログラミング原則は「設計方針の語彙」です。両方を持つことで、ソフトウェアを長く保守可能に育てられます。

参考文献

論文

講義・記事

書籍

解説・補助