1.
はじめに
こんにちは!
今回は SOLID原則の第3回「リスコフの置換原則(LSP)」 です 🎯
名前がとにかく難しそうですが 😇
考え方自体はとてもシンプルなので、安心してください!
2. 🤔 リスコフの置換原則って何?
🧠 一言でいうと…
「子クラスは、親クラスの代わりとして使えていなければならない」
です 👀✨
もう少し噛み砕くと👇
📝 定義(初心者向け)
「親クラスを使っているコードに、子クラスをそのまま置き換えても正しく動作し続けなければならない」
これが リスコフの置換原則(LSP) です 🧩
🧸 身近なイメージで考えてみよう
🎮 ゲームのコントローラーの例
- 「コントローラー」という共通ルールがある
PS用コントローラーでも 🎮
Switch用コントローラーでも 🎮
👉 同じ 「操作できる」前提 で使えるべきですよね?
❌ 途中で
「このボタンは使えません」
「これは急に投げます」
みたいになったら困ります 😱
これが LSP違反のイメージ です。
🚨 LSPを守らないと何が起きる?
-
継承しているのに
👉 使い方が変わる -
親クラス前提のコードで
👉 例外が出る -
if文だらけになる
👉 if (instanceof XXX) { ... }
😵💫 「継承した意味ある?」 状態 になります。
❌ 悪い例(LSP違反)
- 📦 親クラス
class Bird {
fly(): void {
console.log("飛びます 🕊️");
}
}
- 🐧 子クラス(問題あり)
class Penguin extends Bird {
fly(): void {
throw new Error("ペンギンは飛べません ❌");
}
}
😱 何が問題?
function makeBirdFly(bird: Bird) {
bird.fly();
}
makeBirdFly(new Penguin()); // 💥 エラー!
- 👉 Birdとして使っているのに壊れる
- 👉 子クラスが親の約束を破っている
❌ これは リスコフの置換原則違反 です!
✅ 改善例(LSPを守る)
- ✨ 「できること」 で分ける
class Bird {
eat(): void {
console.log("食べます 🍚");
}
}
class FlyingBird extends Bird {
fly(): void {
console.log("飛びます 🕊️");
}
}
class Penguin extends Bird {
// flyは持たない
}
👍 ポイント
- 無理に継承させない
- 「共通の振る舞い」 だけを親に持たせる
- できないことを override しない
👉 これで 安全に置き換え可能 になります 💡
子クラスで、親クラスの振る舞いを書き換えることを「オーバーライド」と呼びます。
3. 🧩 LSPを守るためのチェックリスト ✅
以下に当てはまったら要注意です 🚨
❓ 子クラスで例外を投げている
❓ 親クラスのメソッドを「使えなく」している
❓ 戻り値や振る舞いが変わっている
❓ if (子クラス) が増えている
👉 「本当に is-a 関係?」 を疑いましょう 🤔
4. ℹ️ is-a 関係とは?
「A は B の一種である」 と自然に言える関係 のことです。
🧠 例で考えてみよう
-
🐶 犬 is a 動物
-
🚗 車 is a 乗り物
👉 このように 「○○は△△です」 と日本語で言って、違和感がないかが判断ポイントです ✨
⚠️ is-a が怪しいときに起きる問題
-
親クラスの機能を子クラスで無効化している
-
例外を投げてごまかしている
-
「使えないメソッド」が増える
😵💫 これは LSP違反のサイン です!
✅ is-a を見極める簡単チェック
次の質問を自分にしてみましょう 👇
「この子クラスは、親クラスとしてそのまま使える?」
「振る舞いが変わっていない?」
「◯◯ is a △△」と自信を持って言える?
👉 少しでも「うーん…🤔」となったら
継承ではなく別の設計(分離・委譲)を検討 しましょう。
5. 🎯 コンポジション(委譲)
コンポジション(委譲)とは
-
is-a 関係が成り立つ 👉 LSPを守りやすい ✅
-
is-a が怪しい 👉 LSP違反になりやすい ❌
💡 継承よりも大切な考え方
-
✨ 「継承 = 再利用」 **ではない!
-
継承は 👉 「置き換えられる」 ことが前提
それが怪しいなら
👉 コンポジション(持つ) を使う
// エンジンを表すクラス
// 「エンジンは起動できる」という役割だけを持つ
class Engine {
start() {
// 本来はここでエンジンを始動する処理を書く
console.log("エンジンが起動しました 🚗");
}
}
// 車を表すクラス
class Car {
// コンストラクタで Engine を受け取っている
// これは「Car は Engine を持っている」という関係(has-a)
constructor(private engine: Engine) {}
// 車を動かす処理
drive() {
// エンジンの start メソッドを呼び出しているだけ
// Engine の中身が変わっても、Car のコードは影響を受けにくい
this.engine.start();
console.log("車が走り出しました 🚙");
}
}
🧠 このコードで伝えたいポイント
-
Car extends Engine ではない
❌「車はエンジンの一種」ではない -
Car は Engine を 持っている
✅「車はエンジンを使って動く」
👉 これを 継承ではなくコンポジション(委譲) と呼びます。
「継承は is-a 関係のときだけ使う。
それ以外は、持たせる(コンポジション)方が安全。」
👍 最近の設計ではこちらが主流です。
5. 🎯 まとめ
🧩 LSP = 子クラスは親の代わりになれるべき
❌ 動作が変わる継承は危険
✅ 「できないこと」は継承させない
🌱 継承より設計の意味を考える
6. 📚 次回予告
👉 第4回:インターフェース分離の原則(ISP)
「太りすぎたインターフェース問題」を解決します 🍔➡️🥗
第1回からご覧いただく方はこちら
7. 🥗 おわりに
最後まで読んでいただきありがとうございました!🎉
他の記事もご覧いただけるとすごく嬉しいです。
👍 いいね / 💬 コメントいただけると励みになります!