首页 > 怎样设计Rectangle和Square类才能使之满足OOP的Liskov substitution原则

怎样设计Rectangle和Square类才能使之满足OOP的Liskov substitution原则

Liskov substitution(里氏替换)是oop五原则(SOLID)之一,wikipedia上的介绍如下

Substitutability is a principle in object-oriented programming. It states that, in a computer program, if S is a subtype of T, then objects of type T may be replaced with objects of type S (i.e., objects of type S may be substituted for objects of type T) without altering any of the desirable properties of that program (correctness, task performed, etc.).

其中有提到一个比较经典的违反此原则的案例,那就是square和rectangle,先看代码:

public class Rectangle {

  private double width;
  private double height;

  public void setWidth(double width) {
    this.width = width;
  }

  public void setHeight(double height) {
    this.height = height;
  }

  public double getHeight() {
    return this.height;
  }

  public double getWidth() {
    return this.width;
  }

  public double getPerimeter() {
    return 2*width + 2*height;
  }

  public double getArea() {
    return width * height;
  }

}
public class Square extends Rectangle {

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

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

}

按照自然规则,square是特殊的rectangle(width==height),但是在用两个class来表述时是不符合里氏替换规则的,因为square的setter不满足里氏替换规则里的Postconditions cannot be weakened in a subtype

那么到底怎样设计才行呢?


继承后子类不能改变父类的行为,如果改变了就违反里氏替换原则。正方形从长方形继承违反里氏替换原则是因为在这个例子中正方形和矩形的行为方式不一致。举一个例子,假设矩形width=8,height=4,然后你逐步递减宽,那么你应该看到矩形宽度逐步缩小而高度不变,最后会变成一条线。如果你用正方形来替换这个矩形,那么你原来的程序运行后就会变成宽和高同时缩小,最后变成了一个小点。

B继承A,我们说B is A ,其实应该说是“B的行为 is A ”。另外是否违反这个原则也要看你的程序的运行环境,比如鸟都有羽毛,那么鸵鸟和天鹅都可以从鸟来继承,但是如果你的鸟要能够飞,那么鸵鸟就不能继承鸟,否则违反里氏替换原则,应为鸵鸟不能飞,为什么不能继承,因为鸵鸟没有飞这个行为,是行为不一致。

里氏替换给我们的另一个启示就是不要随便修改父类的方法,因为你要是修改了父类的方法一个搞不好就会违反里氏替换原则(当然了抽象方法除外)。


简单的说一下我的观点:
1、OOP编程中里氏替换原则只是其一,用以强调设计具有继承关系的一系列类时应尽量满足的基本原则;
2、OOP提供了一种封装数据及其操作的编程思想,但并不是现实世界中的所有事物都能被model成对象的,否则也不会出现类似AOP,面向函数的,面向过程的编程思想;
3、在无法很好地对现实世界的事物建模时,组合优先于继承是一个更实用的OOP基本原则;
4、在你所举的例子中,正方形更多的是描述一种长宽相等的长方形的特例。这个长宽相等更多的是一种对象内部属性的约束关系,而不应作为基本属性。否则下次有个长宽需要固定2:1的特殊长方形呢,那到时又该如何处理?


个人看法。

两个选择,取决于你要model怎么样的问题:

  1. Square和Rect是两个类,没有继承关系,如果有的话,都继承自Shape或者四边形;
  2. Rect一个类,包含一个属性,如:
public final boolean isSquare() { return width == height; } 

这个时候,你把Square可以看出一个属性(Property, Decorator, etc.)

同样适合圆形和椭圆形等问题。

究竟是什么样的选择,取决于你的问题本身。比如,你要实现“在长度上压缩”或者“修改长宽比”,明显是第二种情况。而第一种情况,square就是square,rect就是rect,老死不相往来。

另外一个经典的例子是:学生是人,教师是人,但是如果你要model师范学校学生毕业之后参加工作这种情况,就不要把学生和教师作为class,而是人这个class增加一个occupation的属性(property),否则……就会出现让一个学生死掉,用他、她的数据新建一个老师的恐怖情况。

继承不是万能的,也不是唯一实现多态的方法,更不是唯一重用代码的方法,所以Joshua Bloch在他那本Effective Java里面讲Favor composition over inheritance。

【热门文章】
【热门文章】