この記事で分かること
- SOLID原則を「全部適用」するとなぜ逆効果になるのか
- どの原則を、どんな場面で使うべきかの判断フローチャート
- 5つの原則それぞれの「使いどき」と「見送りどき」
- 実際のリファクタリングでS + Iだけを適用した実例
はじめに ― 全部適用して失敗した話
「コードが肥大化してきたので、SOLID原則に従ってリファクタリングしよう」
多くのエンジニアが一度は考えることです。しかし、ここに落とし穴があります。5つの原則を教科書通りにすべて適用した結果、コードがかえって複雑になってしまうというケースが少なくないのです。
ある中規模システムのリファクタリングで、クリーンアーキテクチャをフル適用しようとしたことがあります。ドメイン層・ユースケース層・インターフェース層・インフラ層を厳密に分けた結果、ファイル数が3〜4倍に膨れ上がり、もともと1ファイルに収まっていた見通しの悪さを解消するはずが、今度は 「どのファイルに何が書いてあるか分からない」 という別の見通しの悪さが生まれました。
これはSOLID原則に限らず、設計原則全般に言えることです。原則に従うこと自体が目的になってはいけません。
前提知識 ― SOLID原則30秒おさらい
SOLID原則とは、Robert C. Martinが2000年に提唱したオブジェクト指向設計の5つの原則です(頭字語はMichael Feathersの命名)。本の索引のように、「どこに何があるか」を整理するための指針と考えてください。
| 略称 | 原則名 | 一言でいうと |
|---|---|---|
| S | 単一責任原則(SRP) | 1つのクラス/ファイルには1つの責務だけ |
| O | 開放閉鎖原則(OCP) | 拡張に開き、修正に閉じる |
| L | リスコフ置換原則(LSP) | サブクラスは親クラスと差し替え可能に |
| I | インターフェース分離原則(ISP) | 使わないメソッドへの依存を避ける |
| D | 依存性逆転原則(DIP) | 具体ではなく抽象に依存する |
本題 ― 原則は「痛み」で選ぶ
ここからがこの記事の核心です。
SOLID原則は5つすべてを一度に適用するものではありません。「今、コードのどこが痛いのか」 を起点に、その痛みを解消する最小の原則だけを選びます。
これは「SOLID不要論」ではありません。原則自体は正しいのです。ただし、痛みのないところに薬を塗っても効果はなく、副作用だけが残るということです。
以下のフローチャートで、あなたのコードに今必要な原則を判断できます。
痛みがないなら、適用しないという判断も正解です。
各原則の「使いどき」と「見送りどき」
S(単一責任原則)― ファイルが肥大化したとき
使いどき: 1つのファイルやクラスが複数の理由で変更される状況です。例えば、API定義・ビジネスロジック・データアクセスが1ファイルに混在していて、1,000行を超えている場合。
見送りどき: ファイルが200行以下で、責務が自然にまとまっている場合。無理に分割すると、関連する処理を追うためにファイルを行き来する羽目になります。
# Before: 1ファイルに全部入り
# app.py に API定義 + ビジネスロジック + データ永続化が混在
# After: 責務ごとに分割
# routes/users.py - ユーザー関連API
# routes/orders.py - 注文関連API
# routes/products.py - 商品関連API
# services/order_service.py - 注文のビジネスロジック
O(開放閉鎖原則)― if文が増殖するとき
使いどき: 新しいタイプを追加するたびに既存コードにif/switchを追加している場合。通知機能でSlack/Email/Webhookが増えるたびに分岐が増えるような状況です。
見送りどき: 分岐が2〜3個で安定しており、今後増える見込みがない場合。たとえば本番環境と開発環境の切り替えだけなら、Strategyパターンは過剰です。
L(リスコフ置換原則)― 継承が壊れたとき
使いどき: 親クラスを子クラスに差し替えると予期しない動作やエラーが発生する場合。典型例として、Arrayを継承したReadOnlyListでpushメソッドが例外を投げるような状況があります。
見送りどき: そもそも継承をほとんど使っていない場合。最近のソフトウェア開発では「Composition over Inheritance(継承より合成)」が主流であり、継承を使わないなら意識する場面も少なくなります。
I(インターフェース分離原則)― 巨大クラスの一部しか使わないとき
使いどき: 巨大なインターフェースやクラスのうち、呼び出し側が一部の機能しか使っていない場合。DatabaseManagerがCRUD・バックアップ・マイグレーションを全部持っているなら、分離の候補です。
見送りどき: 1つのクラスの機能が凝集しており、利用側がほぼすべてのメソッドを使っている場合。無理に分割すると概念的に関連した操作が分断され、かえって理解しづらくなります。
D(依存性逆転原則)― テストが書けないとき
使いどき: テスト時にデータベースやAPIなどの外部依存を差し替えられない場合。DIを導入してモックを注入できるようにします。
見送りどき: 開発者が1人で、テストでのモック差し替えの必要性が低い場合。また、言語やフレームワークが柔軟なモック機構を持つ場合(例: Pythonのunittest.mock)は、DIコンテナまで導入しなくても十分テスタブルです。
実例 ― S + I だけで劇的改善
とある小規模システムのバックエンドを例に見てみましょう。
Before: 全部入りの状態
| ファイル | 行数 | 混在している責務 |
|---|---|---|
app.py |
約1,200行 | 全APIエンドポイント + ビジネスロジック + バッチ処理 |
app.js |
約2,000行 | UI・API通信・状態管理・レンダリング |
db.py |
約600行 | ユーザー・タスク・プロジェクト・通知・ログ管理 |
適用した原則と判断理由
- S(単一責任) → ファイルが肥大化している → 採用
- I(IF分離) → 巨大クラスに複数責務が混在 → 採用
- O(開放閉鎖) → プラグイン的拡張の予定がない → 見送り
- L(リスコフ置換) → 継承をほぼ使っていない → 見送り
- D(依存性逆転) → 開発者1人でDIの恩恵が薄い → 見送り
After: S + I だけ適用した結果
routes/
users.py # ユーザーAPI
tasks.py # タスクAPI
projects.py # プロジェクトAPI
reports.py # レポートAPI
stores/
user_store.py # ユーザー管理
task_store.py # タスク管理
project_store.py # プロジェクト管理
2つの原則を適用しただけで、各ファイルが200〜300行に収まり、「どこに何があるか」が一目で分かるようになりました。D/O/Lを無理に適用していたら、インターフェース定義・DIコンテナ・ファクトリーが追加され、ファイル数は今の3倍になっていたでしょう。
よくある落とし穴 ― 「おまじないプログラミング」に注意
教科書通りにフル適用するのは、実は経験が浅い段階でよく見られるパターンです。業界ではこれを 「Cargo Cult Programming(おまじないプログラミング)」 と呼ぶことがあります。リチャード・ファインマンの「Cargo Cult Science」に由来する言葉で、形式だけ真似て本質を理解していない状態を指します。
DHH(Ruby on Railsの作者)もSOLID原則への批判的な見解を示しています。特に「責任」や「変更理由」の定義が曖昧で、極端に適用すると凝集性(関連する処理がまとまっている状態)が失われると指摘しています。
大事なのはこの2つの問いです。
- 今、何が痛いのか?(例:ファイルが肥大化して見通しが悪い)
- その痛みを解消する最小の手段は何か?(例:責務ごとにファイルを分割する)
この2つに答えられれば、使う原則は自然に決まります。
まとめ
- SOLID原則は5つ全部をセットで適用する必要はない
- 痛みのないところに原則を適用しても、複雑さが増すだけ
- 「今何が痛いか」→「その痛みに効く原則はどれか」の順で考える
- 経験豊富なエンジニアほど、この「適材適所」を自然にやっている
- 迷ったら、この記事のフローチャートに立ち返ってみてください
原則は道具です。道具箱から全部出して使うのではなく、今の作業に必要なものだけを選んで使う。それがプロのリファクタリングです。