リスコフの置換原則(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())
この例では、Ostrich
が Bird
を継承しているにもかかわらず、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());
この例では、Square
は Rectangle
を継承していますが、面積の挙動が異なり、親クラスのインターフェース契約を破っています。
✅ 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
この例では、Rectangle
と Square
は Shape
インターフェースを実装する別のエンティティとして扱われており、互いに置換可能ではない前提を回避してLSPを守っています。
まとめ
リスコフの置換原則は、サブクラスが親クラスと互換性を持つことを保証する設計指針です。上級開発者にとっては、継承ではなくインターフェースや委譲を活用して、置換可能性と振る舞いの一貫性を設計する能力が求められます。LSP違反のコードは一見正常に見えても、実行時に不具合を引き起こすため、設計時に注意深く判断することが重要です。