定义

所有引用基类的地方必须能透明地使用其子类的对象。

Functions that use use pointers or references to base classes must be able to use objects of derived classes without knowing it.

多态不就是为了达到这个目标吗?

案例:长方形 is 正方形?

系统中已经存在一个长方形类,现在需要增加一个正方形类,难道正方形不也是长方形吗,小学数学我们都学过,所以这里直接新建一个正方形继承长方形并重写设置宽和高的方法:

  1. // 长方形
  2. public class Rectangle {
  3. protected double width;
  4. protected double height;
  5. public void setWidth(double width) {
  6. this.width = width;
  7. }
  8. public void setHeight(double height) {
  9. this.height = height;
  10. }
  11. public double calculateArea(){
  12. return width * height;
  13. }
  14. }
  15. // 正方形
  16. public class Square extends Rectangle {
  17. @Override
  18. public void setHeight(double height) {
  19. this.height = height;
  20. this.width = height;
  21. }
  22. @Override
  23. public void setWidth(double width) {
  24. this.height = width;
  25. this.width = width;
  26. }
  27. }
  28. // 用户使用场景测试类
  29. public class Test {
  30. public static void main(String[] args) {
  31. testArea(new Rectangle());
  32. }
  33. public static void testArea(Rectangle rectangle) {
  34. rectangle.setHeight(20);
  35. rectangle.setWidth(30);
  36. assert rectangle.calculateArea() == 600;
  37. }
  38. }

上面的代码会出现什么问题?
用户的使用场景是长方形的时候是没有问题的,但是如果是正方形,程序会挂掉。

  1. public class Test {
  2. public static void main(String[] args) {
  3. testArea(new Square()); // 执行失败
  4. }
  5. }

各自的使用者只知道Rectangle或Square,分别使用这两个模型的时候不会存在这个问题,这两个孤立的模型都是有效的。

一旦这两个模型发生了继承关系,相当于组合后构建了一个新的模型,但是对于使用者来说,他的期望是建立在父类Rectangle之上的,而Square继承了父类后,又打破了这个期望,这个新的模型对于用户来说就失效了。

正方形是一个矩形,这个在现实世界中极其合理的关系。而在OO软件设计中,IS-A针对的是对象的行为而言。使用者会对对象的行为作出合理假设,而且是基于父类的行为做出的假设,如果子类的行为跟父类的的行为不兼容,就要当心这个继承的隐患。

  1. 对象的行为方式才是软件真正所关注的问题。
  2. 行为方式是可以进行合理假设的,它是客户程序所依赖的。
  3. 在OOD中,IS-A的关系是就行为而言的。

    契约式设计

    Bertrand Meyer 在 1988 年阐述了 LSP 原则与契约式设计之间的关系。使用契约式设计,类中的方法需要声明前置条件和后置条件。前置条件为真,则方法才能被执行。而在方法调用完成之前,方法本身将确保后置条件也成立。
  • 当通过基类(父类)的接口使用对象时, 用户只知道基类的前置条件和后置条件
  • 派生类(子类) 只能使用相等或者更弱的前置条件类替换父类的前置条件
  • 派生类(子类)只能使用相等或者更强的后置条件来替换父类的后置条件

即相对于子类而言,前置条件要比父类更加宽松,后置条件要更加严格。
在上面的代码中,Rectangle.setWidth(double width)的前置和后置条件:

  1. // 前置条件
  2. Assert (( width instanceof double ) && (width > 0))
  3. // 后置条件
  4. Assert (( this.width == new.width ) && (this.height == old.height))

而Square.setWidth(double width)的前置和后置条件:

  1. // 前置条件
  2. Assert (( width instanceof double ) && (width > 0))
  3. // 后置条件
  4. Assert (( this.width == new.width ) && (this.height == new.width))

明显看出Square的后置条件发生了变化,不符合父类Rectangle定下的契约。