定义
里氏替换原则(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 是继承复用的基石
只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。
-
里氏代换原则是对开闭原则的补充
实现开闭原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。