定义
所有引用基类的地方必须能透明地使用其子类的对象。
Functions that use use pointers or references to base classes must be able to use objects of derived classes without knowing it.
案例:长方形 is 正方形?
系统中已经存在一个长方形类,现在需要增加一个正方形类,难道正方形不也是长方形吗,小学数学我们都学过,所以这里直接新建一个正方形继承长方形并重写设置宽和高的方法:
// 长方形
public class Rectangle {
protected double width;
protected double height;
public void setWidth(double width) {
this.width = width;
}
public void setHeight(double height) {
this.height = height;
}
public double calculateArea(){
return width * height;
}
}
// 正方形
public class Square extends Rectangle {
@Override
public void setHeight(double height) {
this.height = height;
this.width = height;
}
@Override
public void setWidth(double width) {
this.height = width;
this.width = width;
}
}
// 用户使用场景测试类
public class Test {
public static void main(String[] args) {
testArea(new Rectangle());
}
public static void testArea(Rectangle rectangle) {
rectangle.setHeight(20);
rectangle.setWidth(30);
assert rectangle.calculateArea() == 600;
}
}
上面的代码会出现什么问题?
用户的使用场景是长方形的时候是没有问题的,但是如果是正方形,程序会挂掉。
public class Test {
public static void main(String[] args) {
testArea(new Square()); // 执行失败
}
}
各自的使用者只知道Rectangle或Square,分别使用这两个模型的时候不会存在这个问题,这两个孤立的模型都是有效的。
一旦这两个模型发生了继承关系,相当于组合后构建了一个新的模型,但是对于使用者来说,他的期望是建立在父类Rectangle之上的,而Square继承了父类后,又打破了这个期望,这个新的模型对于用户来说就失效了。
正方形是一个矩形,这个在现实世界中极其合理的关系。而在OO软件设计中,IS-A针对的是对象的行为而言。使用者会对对象的行为作出合理假设,而且是基于父类的行为做出的假设,如果子类的行为跟父类的的行为不兼容,就要当心这个继承的隐患。
- 对象的行为方式才是软件真正所关注的问题。
- 行为方式是可以进行合理假设的,它是客户程序所依赖的。
- 在OOD中,IS-A的关系是就行为而言的。
契约式设计
Bertrand Meyer 在 1988 年阐述了 LSP 原则与契约式设计之间的关系。使用契约式设计,类中的方法需要声明前置条件和后置条件。前置条件为真,则方法才能被执行。而在方法调用完成之前,方法本身将确保后置条件也成立。
- 当通过基类(父类)的接口使用对象时, 用户只知道基类的前置条件和后置条件
- 派生类(子类) 只能使用相等或者更弱的前置条件类替换父类的前置条件
- 派生类(子类)只能使用相等或者更强的后置条件来替换父类的后置条件
即相对于子类而言,前置条件要比父类更加宽松,后置条件要更加严格。
在上面的代码中,Rectangle.setWidth(double width)的前置和后置条件:
// 前置条件
Assert (( width instanceof double ) && (width > 0))
// 后置条件
Assert (( this.width == new.width ) && (this.height == old.height))
而Square.setWidth(double width)的前置和后置条件:
// 前置条件
Assert (( width instanceof double ) && (width > 0))
// 后置条件
Assert (( this.width == new.width ) && (this.height == new.width))
明显看出Square的后置条件发生了变化,不符合父类Rectangle定下的契约。