ひとことで言うと#
S・O・L・I・D の頭文字で表される5つの設計原則。クラスやモジュールの責務を明確にし、変更に強く拡張しやすいコードを書くためのオブジェクト指向のバイブル。
押さえておきたい用語#
- SRP(Single Responsibility Principle)
- 単一責任の原則。1つのクラスには1つの変更理由だけを持たせるという原則のこと。「誰がこのコードの変更を要求するか」で責務を分ける。
- OCP(Open/Closed Principle)
- 開放閉鎖の原則。拡張に対して開いていて、修正に対して閉じている設計である。新機能の追加で既存コードを変更しなくて済む状態を目指す。
- LSP(Liskov Substitution Principle)
- リスコフの置換原則。親クラスの代わりに子クラスを使っても、プログラムが正しく動くべきという原則を指す。
- ISP(Interface Segregation Principle)
- インターフェース分離の原則。クライアントが使わないメソッドに依存させない、つまり大きなインターフェースを小さく分割するという原則を指す。
- DIP(Dependency Inversion Principle)
- 依存性逆転の原則。上位モジュールは下位モジュールに直接依存せず、両方とも抽象(インターフェース)に依存するという原則を指す。
SOLID原則の全体像#
こんな悩みに効く#
- 1つのクラスが肥大化して、どこを直しても別の場所が壊れる
- 新しい機能を追加するたびに、既存コードを大量に書き換えなければならない
- 「良い設計」の判断基準が自分の中にない
基本の使い方#
1つのクラスには、1つの変更理由だけを持たせる。
- 「このクラスは何をする責任があるか?」を一言で言えるようにする
- 複数の理由で変更されるクラスは、責務を分割する
- 例:「注文の計算」と「注文のメール通知」は別クラスに
ポイント: 「変更理由」で考えるのがコツ。機能ではなく「誰がこのコードの変更を要求するか」で分ける。
拡張に対して開いていて、修正に対して閉じている設計にする。
- 新しい機能を追加するときに、既存コードを変更しなくて済むようにする
- インターフェースや抽象クラスを使って拡張ポイントを作る
- 例:新しい決済方法の追加は、新クラスの追加だけで対応
ポイント: Strategy パターンやPlugin パターンが典型的な実現手段。
親クラスの代わりに子クラスを使っても、プログラムが正しく動くようにする。
- サブクラスはスーパークラスの契約(事前条件・事後条件)を守る
- 「正方形は長方形の一種」のような直感に反する継承を避ける
- 継承より委譲(コンポジション)を検討する
ポイント: 「is-a」関係に見えても、振る舞いが変わるなら継承は使わない。
ISP: クライアントが使わないメソッドに依存させない。大きなインターフェースは小さく分割する。
DIP: 上位モジュールは下位モジュールに依存しない。両方とも抽象に依存する。
- 「太った」インターフェースは、利用者ごとに分割する
- 具象クラスではなくインターフェースに依存する
- DI(依存性注入)で実装を外部から渡す
ポイント: ISPとDIPはクリーンアーキテクチャの依存関係ルールの土台。
具体例#
状況: 従業員30名のECサイト運営企業。NotificationServiceという1つのクラスが2,400行に肥大化し、メール送信・Slack通知・プッシュ通知・ログ記録を全部やっていた。新しい通知チャネル追加に平均3日、バグ修正で他の通知が壊れることが月2〜3回発生。
SOLID適用:
| 原則 | Before | After |
|---|---|---|
| SRP | NotificationServiceが4つの責務 | EmailSender、SlackNotifier、PushNotifier、NotificationLoggerに分離 |
| OCP | LINE通知追加で既存コードを修正 | Notifierインターフェースを定義、LineNotifier追加だけで対応 |
| LSP | PushNotifierがトークンなしで例外 | トークン検証を別メソッドに切り出し、契約を統一 |
| ISP | NotifierにsendBulk()が不要なクラスにも | BulkNotifierとして別インターフェースに分離 |
| DIP | OrderServiceがEmailSenderを直接new | Notifierインターフェースに依存し、コンストラクタで注入 |
結果: 新しい通知チャネル追加が平均3日→4時間に短縮。通知起因のバグは月2〜3件→月0.2件に激減。
通知チャネル追加3日→4時間、通知起因バグ月2〜3件→0.2件。肥大化した1クラスをSOLID原則で分割しただけでこの結果。
状況: 従業員80名のFinTech企業。決済処理にPaymentProcessorクラスがあり、クレジットカード・銀行振込の2種類に対応していた。新たにQRコード決済・暗号通貨決済の追加が決まったが、既存の巨大なswitch文に分岐を追加する方式に限界を感じていた。
OCP適用:
Before: PaymentProcessor内に巨大switch文(350行)
After: PaymentMethod インターフェースを定義
- CreditCardPayment implements PaymentMethod
- BankTransferPayment implements PaymentMethod
- QRCodePayment implements PaymentMethod(新規追加)
- CryptoPayment implements PaymentMethod(新規追加)| 指標 | Before | After |
|---|---|---|
| 新決済方法の追加工数 | 5人日(テスト込み) | 1.5人日 |
| 追加時の既存コード変更行数 | 平均120行 | 0行(設定ファイル1行のみ) |
| 決済関連バグ(月間) | 4.2件 | 0.8件 |
新決済方法の追加工数5人日→1.5人日、既存コード変更行数0行。switch文をインターフェースに置き換えるだけで、この「触らずに拡張できる」設計が手に入る。
状況: 従業員12名の医療系スタートアップ。患者データ管理システムのPatientServiceが外部の電子カルテAPI、保険API、薬歴APIに直接依存しており、テストを書こうとすると全APIが動いている必要があった。テストカバレッジはわずか8%。
DIP適用:
- 各外部APIのインターフェースを定義(
MedicalRecordRepository,InsuranceGateway,PrescriptionGateway) PatientServiceはインターフェースに依存し、具象クラスはDIコンテナで注入- テスト時はモック実装を注入、本番は実APIクライアントを注入
| 指標 | Before | After |
|---|---|---|
| テストカバレッジ | 8% | 72%(3ヶ月後) |
| テスト実行時間 | 12分(API依存) | 18秒(モック使用) |
| リリース前に発見できるバグ | 全体の20% | 全体の85% |
| 本番障害件数(月間) | 6.3件 | 1.1件 |
テストカバレッジ8%→72%、本番障害月6.3件→1.1件。外部依存をインターフェースに抽象化しただけでこの変化。品質要求の高い医療系だからこそ、DIPの効果が際立つ。
やりがちな失敗パターン#
- 原則を教条的に適用する — すべてのクラスに無理やりインターフェースを作り、ファイル数が倍増。実際に変更が起きそうな箇所に重点的に適用するのが現実的
- SRPの「責任」を細かくしすぎる — 1メソッド1クラスのような極端な分割をしてしまう。「変更理由」が本当に異なるかを基準にする
- 原則の名前は知っているが実践できない — 理論だけ覚えて満足してしまう。既存コードのリファクタリングで練習するのが最も効果的
- 一度に全原則を適用しようとする — 既存コードベースに5原則を一気に適用しようとして大規模リファクタリングに突入する。まずSRPだけを意識して、肥大化した1クラスを分割するところから始める
まとめ#
SOLID原則はオブジェクト指向設計の5つの指針。完璧に守ることが目的ではなく、「なぜこの設計にするのか」を考えるときの判断基準として使うのが正しい。まずはSRP(単一責任)から意識して、肥大化したクラスを分割するところから始めよう。