这是我参与「掘金日新计划 · 2 月更文挑战」的第 6 天,点击查看活动详情
系列文章|源码
定义-是什么
在面向对象的程序设计中,里氏替换原则(Liskov Substitution principle)是对子类型的特别定义。它由芭芭拉·利斯科夫(Barbara Liskov)在1987年在一次会议上名为“数据的抽象与层次”的演说中首先提出。
里氏替换原则的内容可以描述为: “派生类(子类)对象可以在程式中代替其基类(超类)对象。” 以上内容并非利斯科夫的原文,而是译自罗伯特·马丁(Robert Martin)对原文的解读。
芭芭拉·利斯科夫与周以真(Jeannette Wing)在1994年发表论文并提出以上的Liskov代换原则。因此我们一般使用它的另一个通俗版定义:
里氏替换原则(Liskov Substitution Principle, LSP):所有引用基类(父类)的地方必须能透明地使用其子类的对象。
里式替换原则是用来帮助我们在继承关系中进行父子类的设计。
里氏替换原则主要阐述了有关继承的一些原则,也就是什么时候应该使用继承,什么时候不应该使用继承,以及其中蕴含的原理。里氏替换原是继承复用的基础,它反映了基类与子类之间的关系,是对开闭原则的补充,是对实现抽象化具体步骤的规范,由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。
里氏替换原则告诉我们,在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类对象的话,那么它不一定能够使用基类对象。 例如:我喜欢动物,那我一定喜欢狗,因为狗是动物的子类;但是我喜欢狗,不能据此断定我喜欢动物,因为我并不喜欢老鼠,虽然它也是动物。
思考-为什么
- 继承关系给程序带来侵入性;
- 保证程序升级后的兼容性
- 避免程序出错
采用里氏替换原则就是为了减少继承带来的缺点,增强程序的健壮性,版本升级时也可以保持良好的兼容性。即使增加子类,原有的子类也可以继续运行。 里式替换原则的核心就是“约定”,父类与子类的约定。里氏替换原则要求子类在进行设计的时候要遵守父类的一些行为约定。这里的行为约定包括:函数所要实现的功能,对输入、输出、异常的约定,甚至包括注释中一些特殊说明等。
规范的遵循里式替换原则
每个原则有对应代码,详见案例3 github.com/tyronczt/de…
- 子类必须完全实现父类的抽象方法,但不能覆盖父类的非抽象方法;
- 子类可以实现自己特有的方法;
- 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格;
- 子类的实例可以替换任何父类的实例,但反之不成立;
应用-怎么用
案例1:替换的含义
说明一下“替换”的含义
替换的前提是面向对象语言所支持的多态特性,同一个行为具有多个不同表现形式或形态的能力。以JDK的集合框架为例,List接口的定义为有序集合,List接口有多个派生类,比如大家耳熟能详的ArrayList, LinkedList。那当某个方法参数或变量是List接口类型时,既可以是ArrayList的实现, 也可以是LinkedList的实现,这就是替换。
public String addValues(List<String> values) {
return values.get(0);
}
List<String> values = new ArrayList<>();
values.add("a");
values.add("b");
String firstValue = addValues(values);
List<String> values = new LinkedList<>();
values.add("a");
values.add("b");
String firstValue = addValues(values);
案例2:原则重构
通过里式替换原则重构
仓库代码地址:github.com/tyronczt/de…
// 普通用户
public class CommonUser {
private String userId;
private String userName;
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
}
// vip用户
public class VIPUser {
private String userId;
private String userName;
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
}
public class VideoPlayer {
public void play(CommonUser commonUser) {
System.out.println("普通用户播放视频");
}
public void play(VIPUser vipUser) {
System.out.println("vip用户播放视频");
}
}
此处场景为普通用户和vip用户观看视频,不同用户都可播放视频,但是播放视频的具体实现逻辑不同,不如普通用户还需要观看广告,vip不要。为了让系统具有更好的扩展性,同时减少代码重复,使用里氏替换原则对其进行重构。
public abstract class User {
private String userId;
private String userName;
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
}
public class CommonUser extends User {
}
public class VIPUser extends User {
}
public class VideoPlayer {
public void play(User user) {
System.out.println("用户播放视频");
}
}
用UML类图来展示升级过程,UML的快速入门可以参考:【设计模式】十、UML急速入门
案例3:遵循原则
仓库代码地址:github.com/tyronczt/de…
原则1、子类必须完全实现父类的抽象方法,但不能覆盖父类的非抽象方法
public class Principle1 {
public static void main(String[] args) {
Calculator1 c1 = new Calculator1();
int sum = c1.calculate(5, 10);
System.out.println(sum);
SonCalculator1 c2 = new SonCalculator1();
System.out.println(c2.calculate(5, 10));
}
}
class Calculator1 {
public int calculate(int n1, int n2) {
return n1 + n2;
}
}
class SonCalculator1 extends Calculator1 {
public int calculate(int n1, int n2) {
return n1 - n2;
}
}
SonCalculator1
类继承了 Calculator1
类,但覆盖了父类 Calculator1
的 calculate
方法,在 main
方法应用时,调用同样的 calculate
方法,算出来的结果并不相同,不是我们想要的结果,会影响正常的代码开发,再如果 SonCalculator1
还存在子类,再覆写父类的方法,这样会导致程序的继承系统破坏,所有子类不能覆盖父类的非抽象方法。
原则2:子类可以实现自己特有的方法
class Calculator2 {
public int calculate(int n1, int n2) {
return n1 + n2;
}
}
class SonCalculator2 extends Calculator2 {
public int add(int n1, int n2) {
return n1 + n2;
}
public int subtract(int n1, int n2) {
return n1 - n2;
}
}
子类SonCalculator2
,除了父类定义的方法外,还可以新增自己特有的方法,对功能进行拓展。
原则3:当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格
public class Principle3 {
public static ArrayList<String> stringToList(Calculator3 c, String s) {
return c.stringToList(s);
}
}
abstract class Calculator3 {
public abstract ArrayList<String> stringToList(String s);
}
class SonCalculator3 extends Calculator3 {
@Override
public ArrayList<String> stringToList(String s) {
return null;
}
// 代码报错!
// public List<String> stringToList(String s) {
// // do something
// }
}
子类SonCalculator3
继承父类 Calculator3
并重写 stringToList
方法,报错代码中并没有返回 ArrayList
而返回了 List
,导致编译器就报错了。
原则4:子类的实例可以替换任何父类的实例,但反之不成立
public class Principle4 {
public static void main(String[] args) {
Calculator4 c1 = new Calculator4();
int x = 1;
int y = 2;
int addResult = c1.calculate(x, y);
System.out.println(addResult);
SonCalculator4 c2 = new SonCalculator4();
int subtractResult = c2.subtract(x, y);
System.out.println(subtractResult);
}
}
class Calculator4 {
public int calculate(int n1, int n2) {
return n1 + n2;
}
}
class SonCalculator4 extends Calculator4 {
public int subtract(int n1, int n2) {
return n1 - n2;
}
}
子类SonCalculator4
继承父类 Calculator3
并新增 subtract
方法,在方法调用中,子类 c2 也可以使用 calculate
,反之,父类 c1 无法使用 subtract
方法。
注意事项
里式替换原则 = 父类能被子类替换 = 继承复用的规范
不遵守规范 != 当前代码没问题,未来出错率会提高