我们先来看看里氏替换原则LSP的描述。LSP可以简单地描述为:把父类对象替换成它的子类对象,程序的行为没有变化。注意这里的重点,是 “行为没有变化”。
《架构整洁之道》第9章提到“正方形/长方形问题是一个著名(或者说臭名远扬)的违反LSP的设计案例”。
那么,正方形/长方形问题真的违反LSP设计吗?
从数学上看,正方形是一种特殊的长方形,这是没有异议的。一般说A是B的时候,A和B是有继承关系的。放到面向对象语言上来说,正方形也应该归属于长方形,即正方形Square
类是长方形Rectangle
类的子类。
为了说明,给出Rectangle
类和Square
类的定义:
class Rectangle {
int width;
int height;
public Rectangle() {
}
public void setW(int w) {
width = w;
}
public void setH(int h) {
height = h;
}
public int getW() {
return width;
}
public int getH() {
return height;
}
public int area(){
return width * height;
}
}
class Square extends Rectangle {
int side;
public void setS(int s) {
height = width = side = s;
}
public int getS() {
return side;
}
@Override
public void setW(int w) {
setS(w);
}
@Override
public void setH(int h) {
setS(h);
}
}
从代码中可以看到,Rectangle
的行为是可以设置和获取高和宽,还有就是可以根据高和宽计算面积。如果用Square
的对象替换Rectangle
的对象,那么程序的行为没有变化。因为对象还是可以设置和获取高和宽,还是可以根据高和宽计算面积。
既然如此,为什么《架构整洁之道》的作者觉得正方形/长方形问题违反LSP设计?他说错了吗?错在哪里?
《架构整洁之道》第9章中证明正方形/长方形问题违反LSP设计的解释如下:
Square类并不是Rectangle类的子类型,因为Rectangle类的高和宽可以分别修改,而Square类的高和宽则必须一同修改。由于User类始终认为自己在操作Rectangle类,因此会带来一些混淆。例如在下面的代码中:
Rectangle r = …
r.setW(5);
r.setH(2);
assert(r.area()== 10);
很显然,如果上述代码在…处返回的是Square类,则最后的这个assert是不会成立的。
我们先来看看关于assert
的问题,作者说 “如果上述代码在…处返回的是Square类,则最后的这个assert是不会成立的”。不会成立是正常的,因为这个断言的逻辑是有问题的。正确的断言逻辑是assert (r.area() == r.getW() * r.getH());
。这样的话,不管r
对象是Rectangle
类还是Square
类,都是没有问题的。
那么作者的断言逻辑问题出在哪?为什么不能用10
来断言?因为这里的10
是程序行为的结果,而不是表示程序的行为。本文开头就强调了,LSP的重点是 “行为没有变化”,而不是 “行为结果没有变化” 。为什么不能用行为结果来断言呢?因为对象的多态性,多态性让不同对象的相同行为可能产生不同的结果。你不能要求子类的行为结果和父类的行为结果是一样的。
我们再来看看正方形高和宽同时修改的问题。作者说“因为Rectangle类的高和宽可以分别修改,而Square类的高和宽则必须一同修改”,看起来似乎是这么回事儿,正方形改变了长方形的行为。但是“能否分别修改高和宽”是不能作为判断是否是长方形的条件的,你不能说“一个不能分别修改宽和高的长方形”不是长方形。
如果你非要说,只要有行为改变了,就违反LSP。那按照这个思路,就不单是正方形与长方形的问题了,请看下面的DoubleWidthRectangle
类:
class DoubleWidthRectangle extends Rectangle {
@Override
public void setW(int w) {
super.setW(w * 2);
}
}
这个DoubleWidthRectangle
类没有不能分别设置高和宽的问题,只是设置宽的时候会自动乘2。明显如果按照上面提到的思路的话,这个类设置宽时会自动乘2改变了Rectangle
类的行为,那么DoubleWidthRectangle
类违反了LSP设计,不是Rectangle
的子类。
以此类推,将会得出一个结论:“所有重写了父类中有具体实现的行为的子类都是违反LSP设计的”。明显这个结论是有问题的。
至此得证,正方形/长方形问题并没有违反LSP设计,Square
类是Rectangle
类的子类型。