1. 概念
- 任何基类可以出现的地方,子类一定可以出现
2. 为什么要遵循历史替换原则
- 继承关系给程序带来侵入性
- 保证程序升级后的兼容性
- 避免程序出错
3. 规范
- 原则
- 保证基类所拥有的性质在子类中仍然成立
- 子类扩展父类的功能,但是不能改变父类原有的功能
- 如何规范地遵循里氏替换原则
-
子类必须完全实现父类的抽象方法,但不能覆盖父类的非抽象方法
-
子类可以实现自己特有的方法
-
当子类的方法实现父类抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格
-
子类的实例可以替代任何父类的实例,但反之不成立
-
4. 代码示例
示例1
- 不遵循历史替换原则导致程序问题
- 误重写了一个父类的方法导致了程序的问题
package com.bz.design.principle.liskov;
public class Liskov001 {
public static void main(String[] args) {
A a = new A();
System.out.println("11-3=" + a.func1(11, 3));
System.out.println("1-8=" + a.func1(1, 8));
System.out.println("--------------------");
B b = new B();
System.out.println("11-3=" + b.func1(11, 3));
System.out.println("1-8=" + b.func1(1, 8));
System.out.println("11+3+9=" + b.func2(11, 3));
}
}
class A {
public int func1(int num1, int num2) {
return num1 - num2;
}
}
class B extends A {
public int func1(int num1, int num2) {
return num1 + num2;
}
public int func2(int num1, int num2) {
return func1(num1, num2) + 9;
}
}
示例1代码问题分析
- 我们发现原来运行正常的相减功能发生了错误。原因就是:类B无意中重写了父类的方法,造成原有功能出现错误。在实际编程中,我们常常会过通过重写父类的方法完成新的功能,这样写起来虽然简单,但整个继承体系的复用性会比较差。特别是运行多态比较频繁的时候。
- 解决办法(代码参考示例2)
- 通用的做法是:原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉,采用依赖,聚合,组合等关系代替。
示例2
package com.bz.design.principle.liskov.improve;
public class LiskovImprove001 {
public static void main(String[] args) {
A a = new A();
System.out.println("11-3=" + a.func1(11, 3));
System.out.println("1-8=" + a.func1(1, 8));
System.out.println("--------------------");
B b = new B();
// 因为B类不在继承A类,因此调用者,不会再func是求减法
// 调用完成的功能就会很明确
System.out.println("11-3=" + b.func3(11, 3));
System.out.println("1-8=" + b.func3(1, 8));
System.out.println("11+3+9=" + b.func2(11, 3));
}
}
// 创建一个更加基础的基类
class Base {
// 把更加基础的方法和成员写到Base类
}
class A extends Base {
public int func1(int num1, int num2) {
return num1 - num2;
}
}
class B extends Base {
public int func1(int num1, int num2) {
return num1 + num2;
}
public int func2(int num1, int num2) {
return func1(num1, num2) + 9;
}
// 如果B需要使用A类的方法,使用组合关系
private A ao = new A();
public int func3(int num1, int num2) {
return this.ao.func1(num1, num2);
}
}
5. 优缺点
- 优点
- 约束继承泛滥,它也是开闭原则的一种很好的体现。
- 提高了代码的重用性。
- 降低了系统的出错率。类的扩展不会给原类造成影响,降低了代码出错的范围和系统出错的概率。
- 加强程序的健壮性,同时变更时可以做到非常好的兼容性,提高程序的维护性、可扩展性,降低需求变更时引入的风险。
- 缺点
- 继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法。
- 降低代码的灵活性。子类必须拥有父类的属性和方法,让子类自由的世界中多了些约束。
- 增强了耦合性。当父类的常量、变量和方法被修改时,需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果 --> 大段的代码需要重构。
- 继承实际上让两个类耦合性增强了,在适当的情况下,可以通过聚合,组合,依赖来解决问题
6. 注意事项
- 里氏替换原则 = 父类能被子类替换 = 继承复用的规范
- 注意
- 不遵守规范 => 当前代码没问题,未来出错率会提高
- 聚合/组合 > 继承(合成复用原则)