0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

#0096(2025/04/06) リスコフの置換原則(Liskov Substitution Principle: LSP)

Last updated at Posted at 2025-04-06

リスコフの置換原則(Liskov Substitution Principle: LSP)

リスコフの置換原則(Liskov Substitution Principle: LSP)は、オブジェクト指向設計におけるSOLID原則の一つであり、Barbara Liskovによって提唱されました。この原則は、「サブタイプはその親型と置き換えても正しく動作しなければならない」というものです。この記事では、上級プログラマー向けに、LSPが守られていないケースと守られているケースをPythonおよびTypeScriptで紹介します。

LSPの本質

リスコフの置換原則は、継承関係にあるクラスの振る舞いに一貫性を求める原則です。サブクラスが親クラスの契約(前提・期待される動作)を破るような実装を持つと、継承は破綻し、バグや保守性の低下につながります。


Pythonによる例

❌ LSPが破られている例

class Bird:
    def fly(self):
        print("I can fly")

class Ostrich(Bird):
    def fly(self):
        raise NotImplementedError("Ostriches can't fly")

def make_bird_fly(bird: Bird):
    bird.fly()

# 実行時に例外が発生する
make_bird_fly(Ostrich())

この例では、OstrichBird を継承しているにもかかわらず、fly() を呼び出すと例外になります。これは 親クラスの契約に違反しているため、LSP違反です。

✅ LSPが守られている例

from abc import ABC, abstractmethod

class Bird(ABC):
    @abstractmethod
    def move(self):
        pass

class FlyingBird(Bird):
    def move(self):
        print("Flying in the sky")

class WalkingBird(Bird):
    def move(self):
        print("Walking on the ground")

class Sparrow(FlyingBird):
    pass

class Ostrich(WalkingBird):
    pass

def move_bird(bird: Bird):
    bird.move()

# 正常に動作する
move_bird(Sparrow())
move_bird(Ostrich())

この設計では、Bird を抽象クラスにし、動作に応じたサブタイプ(FlyingBird, WalkingBird)に分割しています。move() によって期待される振る舞いは常に一貫しており、LSPを満たしています。


TypeScriptによる例

❌ LSPが破られている例

class Rectangle {
    constructor(public width: number, public height: number) {}

    setWidth(width: number) {
        this.width = width;
    }

    setHeight(height: number) {
        this.height = height;
    }

    getArea(): number {
        return this.width * this.height;
    }
}

class Square extends Rectangle {
    setWidth(width: number) {
        this.width = width;
        this.height = width;
    }

    setHeight(height: number) {
        this.width = height;
        this.height = height;
    }
}

function render(rect: Rectangle) {
    rect.setWidth(4);
    rect.setHeight(5);
    console.log(rect.getArea()); // 期待される出力: 20
}

// しかし、Squareでは 5 * 5 = 25 となる → LSP違反
render(new Square());

この例では、SquareRectangle を継承していますが、面積の挙動が異なり、親クラスのインターフェース契約を破っています。

✅ LSPが守られている例

interface Shape {
    getArea(): number;
}

class Rectangle implements Shape {
    constructor(public width: number, public height: number) {}

    getArea(): number {
        return this.width * this.height;
    }
}

class Square implements Shape {
    constructor(public size: number) {}

    getArea(): number {
        return this.size * this.size;
    }
}

function render(shape: Shape) {
    console.log(shape.getArea());
}

render(new Rectangle(4, 5)); // 20
render(new Square(5));       // 25

この例では、RectangleSquareShape インターフェースを実装する別のエンティティとして扱われており、互いに置換可能ではない前提を回避してLSPを守っています。


まとめ

リスコフの置換原則は、サブクラスが親クラスと互換性を持つことを保証する設計指針です。上級開発者にとっては、継承ではなくインターフェースや委譲を活用して、置換可能性と振る舞いの一貫性を設計する能力が求められます。LSP違反のコードは一見正常に見えても、実行時に不具合を引き起こすため、設計時に注意深く判断することが重要です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?