リスコフの置換原則(The Liskov Substitution Principle: LSP) に違反する例として、Rectangleクラスを継承したSquareクラスの例を複数の書籍で目にしました(Robert (2004)、Michael (2009)、Robert (2018))。何故、Squareクラスはリスコフの置換原則に違反するのでしょうか。
リスコフの置換原則とは、以下のように要約されます。
派生型はその基本型と置換可能でなければならない。(Robert, (2004), p.144)
Rectangleクラスは長方形を表すクラスで、幅(width)と高さ(height)を持ち、幅と高さから面積(area)を計算することができます。
public class Rectangle {
public Rectangle(int width, int height) { /* 略 */ }
private int width;
private int height;
public void setWidth(int width) { /* 略 */ }
public void setHeight(int height) { /* 略 */ }
public int getArea() { /* 略 */ }
}
Squareクラスは正方形を表すクラスで、リスコフの置換原則に違反する例では、Rectangleを継承します。正方形は長方形の一種、というわけです。正方形の幅と高さは定義により常に等しいです。
public Square(int size) {
/* 略 */
}
Squareを親クラスのRectangle型としてインスタンス化してみます。
Rectangle rectangle = new Square(3);
このインスタンスに対して setWidth
と setHeight
を呼んだ時、面積はどうなるでしょうか?
長方形の面積は幅と高さの積で計算されます。一方、正方形は定義により幅と高さが常に等しいので、setHieght(4)
を呼び出した時に、その幅も 3
から 4
に変更されることでしょう。この場合、面積は(Rectangle型なので)3と4の積の12が期待されるにも関わらず、実際には16となります。
rectangle.setWidth(3);
rectangle.setHeight(4); // 正方形の幅と高さは常に等しいので、同時にwidthも4に変更される。
System.out.println(rectangle.getArea()); // => 16
そうすると、RectangleがSquareかそうでないかによって振る舞いが異なってしまい、「派生型はその基本型と置換可能でなければならない」(Robert, (2004), p.144)というリスコフの置換原則に違反しているということになります。
if (rectangle instanceof Square) {
assert rectangle.getArea() == 16;
} else {
assert rectangle.getArea() == 12;
}
書籍「Clean Architecture」では、この問題を「悪名高い」正方形・長方形問題と呼んでいます(Robert, (2018), p.94)。