導入
Liskov Substitution Principle——「子クラスは、親クラスの代わりに使っても正しく動くべき」。継承したのに親の期待を裏切る子は、思わぬバグを生みます。
図解
flowchart TB
P["Bird.Move() の期待<br/>「移動できる」"] --> OK["Sparrow: 飛んで移動 ✅"]
P --> OK2["Penguin: 歩いて移動 ✅<br/>(Fly を強制しないのが正解)"]
P -.-> NG["Fly を全鳥に強制すると<br/>Penguin が例外 → LSP違反"]
style NG fill:#ffebee
サンプル
// ✅ 「移動できる」という親の契約を、どの子も裏切らない
Bird[] birds = { new Sparrow(), new Penguin() };
foreach (Bird b in birds)
b.Move();
// スズメは飛んで移動 / ペンギンは歩いて移動
abstract class Bird
{
public abstract void Move(); // 「飛ぶ」ではなく「移動する」という緩い契約
}
class Sparrow : Bird
{
public override void Move() => Console.WriteLine("スズメは飛んで移動");
}
class Penguin : Bird
{
public override void Move() => Console.WriteLine("ペンギンは歩いて移動");
}
- 子は親として扱われても、親の期待どおりに振る舞うべき
- 「全鳥は飛ぶ」と決めるとペンギンが破綻する→契約を「移動する」に緩めるのが正解
- 継承関係は「子は親の一種として矛盾なく使えるか」で検討する
演習
Account[] accounts = { new Normal(1000), new Savings(1000) };
foreach (var a in accounts)
Console.WriteLine(a.Withdraw(300)); // どちらも残高を返し契約を守る
// TODO: abstract Account(残高 Balance、abstract int Withdraw(int))を作り、
// Normal は「引いた残高」を返す実装をしてください(Savings は用意済みとして Normal のみ)
abstract class Account
{
public int Balance { get; protected set; }
protected Account(int b) { Balance = b; }
public abstract int Withdraw(int amount);
}
class Savings : Account
{
public Savings(int b) : base(b) { }
public override int Withdraw(int amount) { Balance -= amount; return Balance; }
}
___
- 期待される出力:
700700(2行)
ヒント1を見る
Savingsと同じ契約(残高を返す)をNormalでも守ります
ヒント2を見る
class Normal : Account { public Normal(int b) : base(b) { } public override int Withdraw(int amount) { Balance -= amount; return Balance; } }
まとめ
- リスコフの置換原則:子は親の代わりに使っても正しく動く
- 親の契約を子が裏切らないように継承を設計する
- 破綻するなら継承や契約の定義を見直す
次回: 必要な契約だけを持たせる「インターフェース分離の原則」です。