软件质量保障

专注测试圈,自动化测试、测试平台开发、测试新技术、大厂测试岗面经分享, 可以帮忙内推BATJ等大厂!欢迎加VX沟通交流: ISTE1024

在面向对象的语言中,继承是非常优秀的语言机制,它有如下优点:

▪  代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性;

▪  提高代码的重用性;

▪  子类可以形似父类,但又异于父类。

继承也是有缺点的:

▪  继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法;

▪  降低代码的灵活性。子类必须拥有父类的属性和方法,让子类自由的世界中多了些约束;

▪  增强了耦合性。当父类的常量、变量和方法被修改时,需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果——大段的代码需要重构。

今天介绍的里氏替换原则就是帮助开发者如何在开发过程中更好使用继承这一特性解决问题。

定义

什么是里氏替换原则呢?它有两种定义:

▪  第一种定义:If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T,the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.(如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型。)

▪  第二种定义:Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.(所有引用基类的地方必须能透明地使用其子类的对象。)

第二个定义是最清晰明确的,通俗点讲,只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本就不需要知道是父类还是子类。但是,反过来就不行了,有子类出现的地方,父类未必就能适应。

案例

里氏替换原则为良好的继承定义了一个规范,包含了4层含义。

1.子类必须完全实现父类的方法

在大家日常开发中,经常定义接口或抽象类,然后编码实现,调用类则直接传入接口或抽象类,其实这里已经使用了里氏替换原则。举个例子来说明这个原则,大家都打过CS吧,非常经典的FPS类游戏,我们来描述一下里面用到的枪,类图如图所示。

枪的职责是射击,具体是哪种枪(手枪、步枪、机枪)由子类定义。

士兵的职责使用枪来杀敌,而具体使用什么枪来杀敌,就要看士兵有哪些枪,调用的时候才知道。

枪支抽象以及实现类

//枪支抽象类
public abstract class AbstractGun {//枪用来干什么的?杀敌!public abstract void shoot();
}
//手枪、步枪、机枪的实现类
public class Handgun extends AbstractGun {//手枪的特点是携带方便,射程短@Overridepublic void shoot() {System.out.println("手枪射击...");}
}public class Rifle extends AbstractGun{//步枪的特点是射程远,威力大public void shoot(){System.out.println("步枪射击...");}
}
public class MachineGun extends AbstractGun{public void shoot(){System.out.println("机枪扫射...");}
}

士兵的实现类

public class Soldier {//定义士兵的枪支private AbstractGun gun;//给士兵一支枪public void setGun(AbstractGun _gun){this.gun = _gun;}public void killEnemy(){System.out.println("士兵开始杀敌人...");gun.shoot();}
}

场景类

public class Client {public static void main(String[] args) {//产生三毛这个士兵Soldier sanMao = new Soldier();//给三毛一支枪sanMao.setGun(new Rifle());sanMao.killEnemy();}
}

注意:在类中调用其他类时务必要使用父类或接口,如果不能使用父类或接口,则说明类的设计已经违背了LSP原则。

但是现在来新需求了,如果需要增加一把玩具手枪,该如何定义呢?如果在之前的类图上增加一个ToyGun,然后继承AbstracGun类,修改后类图如下:

但是仔细思考,玩具枪是不能用来杀人的,所以玩具枪不能实现AbstracGun类的shoot方法。玩具枪实现代码如下

public class ToyGun extends AbstractGun {//玩具枪是不能射击的,但是编译器又要求实现这个方法,怎么办?虚构一个呗!@Overridepublic void shoot() {//玩具枪不能射击,这个方法就不实现了}
}

场景类:

public class Client {public static void main(String[] args) {//产生三毛这个士兵Soldier sanMao = new Soldier();sanMao.setGun(new ToyGun());sanMao.killEnemy();}
}

坏了,士兵拿着玩具枪来杀敌人,射不出子弹呀!如果在CS游戏中有这种事情发生,那你就等着被人爆头吧,然后看着自己凄惨地倒地。在这种情况下,我们发现业务调用类已经出现了问题,正常的业务逻辑已经不能运行,那怎么办?好办,有两种解决办法:

  1. 在Soldier类中增加instanceof的判断,如果是玩具枪,就不用来杀敌人。【在程序中,每增加一个类,所有与这个父类有关系的类都必须修改】

  2. ToyGun脱离继承,建立一个独立的父类,为了实现代码复用,可以与AbastractGun建立关联委托关系,如图所示

可以在AbstractToy中声明将声音、形状都委托给AbstractGun处理,仿真枪嘛,形状和声音都要和真实的枪一样了,然后两个基类下的子类自由延展,互不影响。

在Java的基础知识中都会讲到继承,Java的三大特征嘛,封装、继承、多态。继承就是告诉你拥有父类的方法和属性,然后你就可以重写父类的方法。按照继承原则,我们上面的玩具枪继承AbstractGun是绝对没有问题的,玩具枪也是枪嘛,但是在具体应用场景中就要考虑下面这个问题了:子类是否能够完整地实现父类的业务,否则就会出现像上面的拿枪杀敌人时却发现是把玩具枪的笑话。

2.子类可以有自己的个性

子类当然可以有自己的行为和外观了,也就是方法和属性,那这里为什么要再提呢?是因为里氏替换原则可以正着用,但是不能反过来用。在子类出现的地方,父类未必就可以胜任。还是以刚才的关于枪支的例子为例,步枪有几个比较“响亮”的型号,比如AK47、AUG狙击步枪等,把这两个型号的枪引入后的Rifle子类图如图所示。

很简单,AUG继承了Rifle类,狙击手(Snipper)则直接使用AUG狙击步枪,源代码如代码清单2-7所示。

AUG狙击枪源码代码

public class AUG extends Rifle {
//狙击枪都携带一个精准的望远镜
public void zoomOut(){System.out.println("通过望远镜察看敌人...");}
public void shoot(){System.out.println("AUG射击...");
}
}

AUG狙击手类的源码代码

public class Snipper { public void killEnemy(AUG aug){//首先看看敌人的情况,别杀死敌人,自己也被人干掉 aug.zoomOut();//开始射击aug.shoot();}
}

狙击手使用狙击枪来杀死敌人,业务场景Client类的源代码如代码清单所示。

狙击手使用AUG杀死敌人

public class Client {     public static void main(String[] args) {             //产生三毛这个狙击手             Snipper sanMao = new Snipper();             sanMao.setRifle(new AUG());             sanMao.killEnemy();     }
}

狙击手使用G3杀死敌人,运行结果如下所示:

通过望远镜察看敌人...

AUG射击...

在这里,系统直接调用了子类,狙击手是很依赖枪支的,别说换一个型号的枪了,就是换一个同型号的枪也会影响射击,所以这里就直接把子类传递了进来。这个时候,我们能不能直接使用父类传递进来呢?修改一下Client类,如代码清单所示。

使用父类作为参数

public class Client {     public static void main(String[] args) {             //产生三毛这个狙击手             Snipper sanMao = new Snipper();            sanMao.setRifle((AUG)(new Rifle()));             sanMao.killEnemy();     }
}

显示是不行的,会在运行期抛出java.lang.ClassCastException异常,这也是大家经常说的向下转型(downcast)是不安全的,从里氏替换原则来看,就是有子类出现的地方父类未必就可以出现。

3.覆盖或实现父类的方法时输入参数可以被放大

方法中的输入参数称为前置条件,这是什么意思呢?大家做过Web Service开发就应该知道有一个“契约优先”的原则,也就是先定义出WSDL接口,制定好双方的开发协议,然后再各自实现。里氏替换原则也要求制定一个契约,就是父类或接口,这种设计方法也叫做Design by Contract(契约设计),与里氏替换原则有着异曲同工之妙。契约制定了,也就同时制定了前置条件和后置条件,前置条件就是你要让我执行,就必须满足我的条件;后置条件就是我执行完了需要反馈,标准是什么。这个比较难理解,我们来看一个例子,我们先定义一个Father类,如代码清单所示。

//Father类源代码
public class Father {     public Collection doSomething(HashMap map){             System.out.println("父类被执行...");             return map.values();     }
}

这个类非常简单,就是把HashMap转换为Collection集合类型,然后再定义一个子类,源代码如代码所示。

//子类源代码
public class Son extends Father {     //放大输入参数类型     public Collection doSomething(Map map){             System.out.println("子类被执行...");             return map.values();     }
}

父类和子类都已经声明了,场景类的调用如代码清单所示。

//场景类源代码
public class Client {     public static void invoker(){             //父类存在的地方,子类就应该能够存在             Father f = new Father();             HashMap map = new HashMap();             f.doSomething(map);     }     public static void main(String[] args) {             invoker();     }
}

代码运行后的结果如下所示:

父类被执行...

根据里氏替换原则,父类出现的地方子类就可以出现,我们把上面的粗体部分修改为子类,如代码清单所示。

//子类替换父类后的源代码
public class Client {     public static void invoker(){             //父类存在的地方,子类就应该能够存在             Son f =new Son();             HashMap map = new HashMap();             f.doSomething(map);     }     public static void main(String[] args) {             invoker();     }
}

运行结果还是一样,看明白是怎么回事了吗?父类方法的输入参数是HashMap类型,子类的输入参数是Map类型,也就是说子类的输入参数类型的范围扩大了,子类代替父类传递到调用者中,子类的方法永远都不会被执行。这是正确的,如果你想让子类的方法运行,就必须覆写父类的方法。大家可以这样想,在一个Invoker类中关联了一个父类,调用了一个父类的方法,子类可以覆写这个方法,也可以重载这个方法,前提是要扩大这个前置条件,就是输入参数的类型宽于父类的类型覆盖范围。这样说可能比较难理解,我们再反过来想一下,如果Father类的输入参数类型宽于子类的输入参数类型,会出现什么问题呢?会出现父类存在的地方,子类就未必可以存在,因为一旦把子类作为参数传入,调用者就很可能进入子类的方法范畴。

4.覆写或实现父类的方法时输出结果可以被缩小

这是什么意思呢,父类的一个方法的返回值是一个类型T,子类的相同方法(重载或覆写)的返回值为S,那么里氏替换原则就要求S必须小于等于T,也就是说,要么S和T是同一个类型,要么S是T的子类,为什么呢?分两种情况,如果是覆写,父类和子类的同名方法的输入参数是相同的,两个方法的范围值S小于等于T,这是覆写的要求,这才是重中之重,子类覆写父类的方法,天经地义。如果是重载,则要求方法的输入参数类型或数量不相同,在里氏替换原则要求下,就是子类的输入参数宽于或等于父类的输入参数,也就是说你写的这个方法是不会被调用的,参考上面讲的前置条件。

采用里氏替换原则的目的就是增强程序的健壮性,版本升级时也可以保持非常好的兼容性。即使增加子类,原有的子类还可以继续运行。在实际项目中,每个子类对应不同的业务含义,使用父类作为参数,传递不同的子类完成不同的业务逻辑,非常完美!

最佳实践

在项目中,采用里氏替换原则时,尽量避免子类的“个性”,一旦子类有“个性”,这个子类和父类之间的关系就很难调和了,把子类当做父类使用,子类的“个性”被抹杀——委屈了点;把子类单独作为一个业务来使用,则会让代码间的耦合关系变得扑朔迷离——缺乏类替换的标准。

内推福利

扫码投递简历(校园招聘)

社招需要内推的可以直接联系我or私信我(VX: ISTE1024

好文推荐

往期推荐

多项目管理实践论坛定于12月11-12日通过云端分享

经验分享|测试工程师转型测试开发历程

技术面必考:多线程、多进程

接口测试框架开发实践2:接口自动化测试框架设计思路

接口自动化测试框架实践1:接口测试概述

我在阿里做测开

设计模式|LSP(里氏替换)原则相关推荐

  1. 北风设计模式课程---里氏替换原则(Liskov Substitution Principle)

    北风设计模式课程---里氏替换原则(Liskov Substitution Principle) 一.总结 一句话总结: 当衍生类能够完全替代它们的基类时:(Liskov Substitution P ...

  2. 设计模式之里氏替换原则示例

    设计模式之里氏替换原则示例 里氏替换原则强调的是设计和实现要依赖于抽象而非具体:子类只能去扩展基类,而不是隐藏或者覆盖基类,它包含4层含义. 一.里氏替换4原则 1.子类可以实现父类的抽象方法,但不能 ...

  3. 设计模式:里氏替换原则

    里氏替换原则(Liskov Substitution Principle ,LSP): 指的是任何基类可以出现的地方,子类一定可以出现. 定义1 如果对每一个类型为T1的对象o1,都有类型为T2的对象 ...

  4. 设计模式之 里氏替换原则

    里氏替换原则(Liskov Substitution Principle, LSP):所有引用父类的地方必须能使用其子类的对象. 简单来说就是 子类拥有父类所拥有的一切,并可以在此基础上进而扩展其他的 ...

  5. 设计模式:里氏替换原则(详解)

    里氏替换原则 前言 一. 介绍 二.代码演示 1.版本一:原始版本 2.版本二:里氏替换原则 前言 本博主将用CSDN记录软件开发求学之路上亲身所得与所学的心得与知识,有兴趣的小伙伴可以关注博主!也许 ...

  6. 设计模式之里氏替换原则

    引入里氏替换原则(LiskovSubstitution Principle,LSP)的目的就是为了解决继承中父子类之间产生的强耦合性. 2.1 爱恨纠葛的父子关系 继承 在面向对象的语言中,继承是必不 ...

  7. Java设计模式(03) -- 里氏替换原则

    六大设计原则 单一职责原则定义:约定一个类应该有且仅有一个改变类的原因: 开闭原则定义:规定软件中的对象.类.模块和函数对扩展应该是开放的,但对于修改是封闭的,核心思想也可以理解为面向抽象编程. 里氏 ...

  8. 《设计模式》——里氏替换原则

    先扯两句 原本是不想扯了的,因为很久没扯了也不知道该说写什么,可是这里氏替换原则东西实在是太多了,我看过都快一周了,但是每次想写博客的时候,都写几个字就扔下了,倒不是说书中的内容不够详细,只是如果都是 ...

  9. Java设计模式之里氏替换原则(Liskov Substitution principle)

    2019独角兽企业重金招聘Python工程师标准>>> Java是面向对象的语言,那么什么是面向对象程序设计呢? 面向对象程序设计(英语:Object-oriented progra ...

最新文章

  1. 2021年人工神经网络第一次作业:参考答案-1
  2. python3 报错 TypeError: load() got an unexpected keyword argument ‘encoding‘ 解决方法
  3. mac终端输入密码被阻止
  4. mysql错误消息1_MySQL出错信息: Subquery returns more than 1 row及其解决方法
  5. 案例:控制文件序列号满故障处理和分析
  6. 前端有未来吗?听我娓娓道来!
  7. 使用PHP脚本远程部署git项目
  8. 同济大学软件学院院长谈嵌入式方向选择
  9. 解决Java提示“编码GBK的不可映射字符”的问题
  10. 计算机flash听课记录范文,听课记录范文.doc
  11. wordpress页脚添加备案号等版权信息
  12. 互联网金融热浪下丨看看美国的互联网金融怎么玩?
  13. java实现mysql的导入导出_Java实现mysql导入导出Excel
  14. 数据结构-第一讲 基本概念-学习笔记(MOOC 浙江大学 陈越 何钦铭)
  15. TCP协议RST:RST介绍、什么时候发送RST包
  16. P3456 [POI2007]GRZ-Ridges and Valleys
  17. 【python】pygame实现植物大战僵尸小游戏(附源码 有注释)
  18. 一别两宽,各生欢喜。
  19. Linux获取当前目录名,shell获取当前目录名
  20. 淘宝卖家掌握最新操作方法,让你的直通车飞起来!

热门文章

  1. 研发自主可控的国产联盟链:他们与多家知名机构合作落地项目
  2. 优秀IT项目经理的六大特质_转帖IT专家网
  3. Win10修改msconfig后无法开机怎么办?
  4. input禁止光标_input 不可输入,且禁止光标显示的几种方案兼容iphone5(se)
  5. ArcGIS 在洪水灾害普查、风险评估及淹没制图中的实践技术应用
  6. 安装MeadCo ScriptX的ActiveX控件
  7. 鲲鹏与天宫:从航空城驶向智能时代的万千星辰
  8. 神级以下必读! ~程序员学习资料(图书源码) 01
  9. 悟透delphi 第二章 DELPHI与WIN32时空
  10. php注册推荐码,javascript - PHP注册邀请码