软件修养 -- 里氏替换原则(LSP:Liskov Substitution Principle)

定义

里氏替换原则(LSP:Liskov Substitution Principle):派生类(子类)对象可以在程序中代替其基类(超类)对象

由来

有一功能 P1, 由类 A 完成,现需要将功能 P1 进行扩展,扩展后的功能为 P,其中 P 由原有功能 P1 与新功能 P2 组成。新功能 P 由类 A 的子类 B 来完成,则子类 B 在完成新功能 P2 的同时,有可能会导致原有功能 P1 发生故障。

解决方案

当使用继承时候,类 B 继承类 A 时,除添加新的方法完成新增功能 P2,尽量不要修改父类方法预期的行为

优点

  • 里氏替换原则是实现开闭原则的重要方式之一
  • 里氏替换原则克服了继承中重写父类造成的可复用性变差的缺点
  • 里氏替换原则是动作正确性的保证。即类的扩展不会给已有的系统引入新的错误,降低了代码出错的可能性
  • 加强程序的健壮性,同时变更时可以做到非常好的兼容性,提高程序的维护性、可扩展性,降低需求变更时引入的风险

思考

  • 里氏替换原则在阐述什么?

    里氏替换原则主要阐述了有关继承的一些原则,也就是什么时候应该使用继承,什么时候不应该使用继承,以及其中蕴含的原理

  • 子类可代替基类是什么意思?

    任何基类可以出现的地方,子类一定可以出现

  • 如何遵循里氏替换原则?

    • 子类必须完全实现父类的抽象方法,但不能覆盖父类的非抽象方法
    • 子类可以实现自己特有的方法
    • 当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松
    • 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格类向上转换是安全的,向下转换则不一定是安全
    • 子类的实例可以替代任何父类的实例,但反之不成立

案例

需要完成一个两数相减的功能,代码如下:

1
2
3
4
5
class A {
  func1(a: number, b: number) {
    return a - b;
  }
}

后来,需要增加一个新的功能:完成两数相加,然后再与 100 求和,由类 B 来负责。即类 B 需要完成两个功能:

  • 两数相减
  • 两数相加,然后再加 100
1
2
3
4
5
6
7
8
9
class B extends A {
  func1(a: number, b: number) {
    return a + b;
  }

  func2(a: number, b: number) {
    return this.func1(a, b) + 100;
  }
}

这时发现原本运行正常的相减功能发生了错误,原因就是类 B 在给方法起名时无意中重写了父类的方法,造成了所有运行相减功能的代码全部调用了类 B 重写后的方法,造成原来运行正常的功能出现了错误。在实际编程中,经常会通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是这样往往也增加了重写父类方法所带来的风险。

正确的做法应该是不重写父类的方法,通过扩展新方法来实现需求,代码如下:

1
2
3
4
5
class B extends A {
  func2(a: number, b: number) {
    return a + b + 100;
  }
}

注意点

  • 尽量不要重写父类的方法

    子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。里氏替换原则的重点在不影响原功能,而不是不覆盖原方法

  • LSP 是继承复用的基石

    只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。

  • 里氏代换原则是对开闭原则的补充

    实现开闭原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。

延伸阅读


CatchZeng
Written by CatchZeng Follow
AI (Machine Learning) and DevOps enthusiast.