Liskov’s Substitution Principle
动机
我们进行模块设计时一般都是先建立一些继承体系,也就是一些抽象基类,然后新建派生类来扩展功能。
我们必须确保新的派生子类只是扩展基类的功能而没有将其替换掉。不然的话,在现有模块引入新建的类的时候可能会产生副作用。
LSP 规定如果程序中模块使用的是基类,那么基类的引用可以替换成子类,而不会影响到模块功能。
目的
派生子类必须能完全替代其基类。个人理解是,子类和基类对外暴露的行为必须一致。
例子
以下是违背里氏替换原则的一个经典例子。例子中用到了2个类:Retangle
和 Square
。我们假设在程序某处使用到了Retangle
实例。 接着我们扩展应用,增加 Square
类。Square
由工厂模式根据一些条件返回,我们不清楚具体返回什么类型。不过,我们知道返回的肯定是 Retangle
。我们获取 Retangle
对象实例,把宽设成5,高设成10,然后得到面积值。对于长方形而言,宽为5,高为10的话面积应该是50,但是结果却是100。 这里行为就产生了分歧。
// 违背 LSP
class Rectangle {
protected int m_width;
protected int m_height;
public void setWidth(int width) {
m_width = width;
}
public void setHeight(int height) {
m_height = height;
}
public int getWidth() {
return m_width;
}
public int getHeight() {
return m_height;
}
public int getArea() {
return m_width * m_height;
}
}
class Square extends Rectangle {
public void setWidth(int width) {
m_width = width;
m_height = width;
}
public void setHeight(int height) {
m_width = height;
m_height = height;
}
}
class LspTest {
private static Rectangle getNewRectangle() {
// 假设这是由一个工厂来生产的
return new Square();
}
public static void main(String[] args) {
Rectangle r = LspTest.getNewRectangle();
r.setHeight(10);
r.setWidth(5);
// 用户知道这是一个长方形, 会以为自己能够分别设置长和宽
// 并且期望得到 长 * 宽 的结果, 然后就懵逼了
System.out.print(r.getArea());
}
结论
里氏替换原则是对开放-关闭原则的扩展,它要求我们必须确保新添加的子类在扩展基类功能的同时不会改变它们的行为。