里氏替换

里氏替换原则

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

该原则由麻省理工学院的 Barbara Liskov 女士提出,即使用基类的指针或引用的函数,必须是在不知情的情况下,能够使用派生类的对象。父类能够替换子类,但子类不一定能替换父类。也就是说,在代码中可以将父类全部替换为子类,程序不会报错,也不会在运行时出现任何异常,但反过来却不一定成立。学过 OO 的同学都知道,子类本来就可以替换父类,为什么还要里氏替换原则呢?这里强调的不是编译错误,而是程序运行时的正确性。程序运行的正确性通常可以分为两类。一类是不能出现运行时异常,最典型的是 UnsupportedOperationException,也就是子类不支持父类的方法。第二类是业务的正确性,这取决于业务上下文。

下例中,由于 java.sql.Date 不支持父类的 toInstance 方法,当父类被它替换时,程序无法正常运行,破坏了父类与调用方的契约,因此违反了里氏替换原则。

package java.sql;

public class Date extends java.util.Date {

  @Override
  public Instant toInstant() {
    throw new java.lang.UnsupportedOperationException();
  }
}

该原则包含以下几层要求:

  • 子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法,子类可以增加自己独有的方法。
  • 当子类的方法重载父类的方法时候,方法的形参要比父类的方法的输入参数更加宽松。
  • 当子类的方法实现父类的抽象方法时,方法的返回值要比父类更严格。

里氏替换原则之所以这样要求是因为继承有很多缺点,他虽然是复用代码的一种方法,但同时继承在一定程度上违反了封装。父类的属性和方法对子类都是透明的,子类可以随意修改父类的成员。这也导致了,如果需求变更,子类对父类的方法进行一些复写的时候,其他的子类无法正常工作。

如果你的设计满足里氏替换原则,那么子类(或接口的实现类)就可以保证正确性的前提下替换父类(或接口),改变系统的行为,从而实现扩展。BranchByAbstraction 和绞杀者模式 都是基于里氏替换原则,实现系统扩展和演进。这也就是对修改封闭,对扩展开放,因此里氏替换原则是实现开闭原则的一种解决方案。

上一页
下一页