面接で「SOLID原則を説明してください」と聞かれたことは、皆さん一度はあるのではないでしょうか。
しかし正直なところ、5つを順番に暗唱して答えられても、「では自分のコードにどう使うのか」まで話せる人は多くありません。
今回はSOLIDを教科書的な定義ではなく、実務の場面に置き換えて説明します。分量が多いため、2回に分けます。今日は前半の3文字、SRP・OCP・LSPです。
SOLIDは、Robert Martin(Uncle Bob)が整理したオブジェクト指向設計の5原則の頭文字です。
- SRP — 単一責任の原則
- OCP — 開放・閉鎖の原則
- LSP — リスコフの置換原則
- ISP — インターフェース分離の原則
- DIP — 依存性逆転の原則
5つはそれぞれ別の話に見えますが、共通するメッセージは1つです。変更が起きたとき、修正が必要なコードの範囲を小さくすることです。
SRP — 単一責任の原則
教科書的な定義は「クラスはただ1つの責務だけを持つべき」ですが、Uncle Bob自身が後に、よりよい表現へ言い換えました。
モジュールは、ただ1つのアクターに対してのみ責務を負うべきである。
ここでいうアクターとは、そのコードの変更を要求する人や部署です。たとえば、次のようなクラスがあるとします。
class Employee {
func calculatePay() { ... } // 経理チームから変更依頼
func reportHours() { ... } // 人事チームから変更依頼
func save() { ... } // 開発チーム(DBA)から変更依頼
}
3つのメソッドは、それぞれ所有者が異なります。経理チームの要求でcalculatePayを修正する際に共通ロジックへ触れると、人事チームのレポートがひそかに間違ってしまいます。実際にこうした事故が起きると、原因を見つけるのも困難です。
SRPはこれを「変更を求める人が異なるなら、コードも分離する」と整理してくれます。給与計算、勤務レポート、保存をそれぞれ別のクラスに分ければ、経理チームの要求が人事機能に影響することはありません。
OCP — 開放・閉鎖の原則
定義は「拡張に対して開かれ、修正に対して閉じていなければならない」です。最初は禅問答のように聞こえますが、コードで見ると明快です。
// 決済手段が増えるたびにこの関数を修正する必要があるなら
func pay(method: String, amount: Int) {
if method == "card" { ... }
else if method == "kakaopay" { ... }
else if method == "naverpay" { ... }
// 新しい決済手段 = else if = 既存コードを修正
}
この構造では、Toss Payを追加するために、正常に動いている関数を開いて修正しなければなりません。修正のたびに、既存の決済手段を壊すリスクを負うことになります。
// 新しい決済手段を「追加」するだけで済む構造
let payments: [String: Payment] = [
"card": CardPayment(),
"kakaopay": KakaoPayment(),
"naverpay": NaverPayment(),
// Toss Payの追加 = 新しいクラスを登録する1行
]
func pay(method: String, amount: Int) {
payments[method]?.process(amount: amount)
}
既存コードには触れず(修正に対して閉じ)、新しいクラスを追加するだけで機能が増えます(拡張に対して開く)。頻繁に変わる箇所にだけこの構造を用意するのがコツです。すべてのコードに適用すると、前回説明したYAGNI違反になります。
LSP — リスコフの置換原則
名前は最も恐ろしく見えますが、意味は簡単です。
子クラスは、親クラスが使われる場所にそのまま置き換えても、プログラムを壊してはなりません。
有名な反例が、長方形と正方形の問題です。「正方形は長方形である」という数学的常識どおりに継承すると、次のようなことが起きます。
class Rectangle {
var width = 0
var height = 0
func setWidth(_ w: Int) { width = w }
func setHeight(_ h: Int) { height = h }
}
class Square: Rectangle {
override func setWidth(_ w: Int) { width = w; height = w } // 正方形だから
override func setHeight(_ h: Int) { width = h; height = h }
}
// 長方形を期待するコード
func test(_ rect: Rectangle) {
rect.setWidth(5)
rect.setHeight(4)
print(rect.width * rect.height) // 20を期待するが Squareなら 16
}
Squareを入れた瞬間、「幅5、高さ4なら面積20」という当然の期待が崩れます。is-a関係が成り立っているように見えても、振る舞いの契約を壊すなら継承してはいけない、というのがLSPです。
実務上のサインはこれです。子クラスで親メソッドをオーバーライドし、例外を投げたり空実装にしたりしているなら、その継承はLSP違反である可能性が高いでしょう。
前編を終えて
今日扱った3つの原則を、1行ずつ残しておきます。
- SRP — 変更を求める人が異なるなら、コードも分離する。
- OCP — 頻繁に変わる箇所は、修正ではなく追加で対応できるようにする。
- LSP — 子クラスは親クラスの契約を壊さない。壊す必要があるなら、継承が間違っている。
次回は残りの2文字、ISP(インターフェース分離)とDIP(依存性逆転)を扱います。特にDIPは、最近のフレームワークがなぜどれも依存性注入を使うのかと直接つながる話なので、きっと面白いはずです。

![[SOLID #1] SOLIDの5文字を暗記せず理解する(SRP・OCP・LSP)のカバー画像](/assets/images/posts/7b95a0fb-b586-4f62-b56e-4a8b31a29dfd/1.jpg)