本文へスキップ
BecomeCoder

C#オブジェクト指向コース · 第3章 SOLID原則 · レッスン23

リスコフの置換原則(L)

ブラウザで完結

導入

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; }
}
___
  • 期待される出力: 700 700(2行)
ヒント1を見る

Savingsと同じ契約(残高を返す)をNormalでも守ります

ヒント2を見る

class Normal : Account { public Normal(int b) : base(b) { } public override int Withdraw(int amount) { Balance -= amount; return Balance; } }

まとめ

  • リスコフの置換原則:子は親の代わりに使っても正しく動く
  • 親の契約を子が裏切らないように継承を設計する
  • 破綻するなら継承や契約の定義を見直す

次回: 必要な契約だけを持たせる「インターフェース分離の原則」です。

実際に動かしてみよう

本文のサンプルや演習のコードは、コードブロック右上の「コピー」ボタンでコピーして、下のエディタに貼り付ければそのまま実行できます。

C# — ブラウザ内で実行

ブラウザ内でC#を動かす環境を読み込みます(初回のみ数秒)。
スクロールして表示された時点でも自動で読み込まれます。