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

LSP:Liskov Substitution Principle

派生型はその基本型と置換可能でなければならない


リスコフの置換原則は、継承に関する原則である。
継承は、OCPの要である「抽象」と「ポリモーフィズム」をサポートする鍵となるメカニズムである為、OCPと関係のある原則であるといえる。
さらにいえば、LSPに違反すれば必然的にOCPにも違反していると考えることができる。

リスコフの原則に違反する例

次の例は、リスコフの置換原則に違反しているといえる。

public class abstract Shape {
	protected int lineColor;
	protected int bgColor;
}
public class Circle extends Shape {
	public void drawCircle() {
		・・・
	}
}
public class Square extends Shape {
	public void drawSquare() {
		・・・
	}
}
public class Draw {
	public void draw(Shape s) {
		if (s instanceof Circle) {
			s.drawCircle();
		} else if (s instanceof Square) {
			s.drawSquare();
		}
	}
}

まず、Draw.draw()はOCPに違反している。
新たにShapeの派生クラスが追加された場合に、Draw.draw()の変更が必要であり、変更に閉じていない。
また、Circle.drawCircle()とSquare.drawSquare()はShapeのメソッドをオーバーライドしているわけではない為、CircleとSquareはShapeの代わりとしては使用できず、RSPにも違反してしまっている。

これがもしShape.draw()という抽象メソッドが存在し、Circle.drawCircle()とSquare.drawSquare()がそれぞれShape.draw()をオーバーライドする形で実装されているのなら、Draw.draw()内で型によるif・elseは不要となり、OCPとLISP準じた格好となる。

「正当性」と「本来的な性質」は別物

次のような長方形クラスがあるとする。

public class Rectangle {
	private int height;
	private int width;
	
	// height,widthのgetter/setter
	・・・・
	
	public int getArea() {
		return height * width;
	}
}

新たに正方形クラスを追加しようとした場合、通常Rectangleを継承して作る考えが思い浮かぶ。
その場合、問題となるのは長方形クラスのheightとwidthは、正方形クラスでは2つも必要なく、それぞれ異なる値を設定されてしまうと正方形としての正当性が損なわれてしまうということだ。
そこで、次のように正方形クラスのsetterを実装する。

public void setHeight(int height) {
	this.height = this.width = height;
}
public void setWidth(int width) {
	this.width = this.height = width;
}

一見問題なさそうだが、実はLSPに違反している。
実際、次のような関数で問題が発生する

void g(Rectangle r) {
	int height = 4;
	int width = 5;
	r.setHeight(height);
	r.setWidth(width);
	assert(r.getArea() == (height * width));
}

メソッドgの作者は、Rectangleのheightとwidthは独立していると考えており、heightを変更した場合にwidthまで変更されてしまうということは知らないのだ。
この場合、正方形クラスは長方形クラスと振る舞いが異なってしまっている為、長方形クラスの代わりとなれず、LSPに違反してしまう。

正方形クラスの作成者の立場からみると正方形の性質は数学的に正しく保たれているとモデルの正当性を証明できる。
しかし、立場が変えてメソッドgの作成者が求める長方形クラスの正当性は別のところにあった。
このように、モデルの正当性は立場によって異なる為、立場抜きで正当性を証明することは意味をなさない。
また、正当性はユーザの立場に立って考えるべきである。
したがって、正方形クラスの作者は、正方形クラスは長方形クラスの正当な派生型ではないことをまず認めなければならない。
その上で、LSPに準じた形へ変更する。
この場合であれば、例えば、まず長方形クラスの抽象クラスとして、getArea()を持つ四角形クラスを新たに作成し、長方形クラスと正方形クラスを派生クラスとする。
そして、height、widthの設定は長方形クラスと正方形クラスへそれぞれ用意するなどといった変更が考えられる。
長方形と正方形は基本的に似た性質を持っている為、共通する部分は抽象クラスへ抜き出し、問題となるheight、widthの設定は各自実装する。

また、このように抽象クラスと派生クラスで振る舞いが異なる場合は意外と多い。
特に基本クラスで投げない例外を派生クラスで投げてしまうような場合は、基本クラスの代用ができなくなってしまう為、ユーザに注意を喚起するか、派生クラスが例外を投げないようにする必要がある。