极客时间:王争:设计模式课程专栏,如有侵权,联系立即删除

如何理解“里式替换原则”?

里式替换原则的英文翻译是:Liskov Substitution Principle,缩写为 LSP

英语我就不贴了,讲人话:子类对象(object of subtype/derived class)能够替换程序(program)中父类对象(object of base/parent class)出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏。

来个例子好理解

如下代码中,父类 Transporter 使用 org.apache.http 库中的 HttpClient 类来传输网络数据。子类 SecurityTransporter 继承父类 Transporter,增加了额外的功能,支持传输 appId 和 appToken 安全认证信息。

public class Transporter {private HttpClient httpClient;public Transporter(HttpClient httpClient) {this.httpClient = httpClient;}public Response sendRequest(Request request) {// ...use httpClient to send request}
}public class SecurityTransporter extends Transporter {private String appId;private String appToken;public SecurityTransporter(HttpClient httpClient, String appId, String appToken) {super(httpClient);this.appId = appId;this.appToken = appToken;}@Overridepublic Response sendRequest(Request request) {if (StringUtils.isNotBlank(appId) && StringUtils.isNotBlank(appToken)) {request.addPayload("app-id", appId);request.addPayload("app-token", appToken);}return super.sendRequest(request);}
}public class Demo {    public void demoFunction(Transporter transporter) {    Reuqest request = new Request();//...省略设置request中数据值的代码...Response response = transporter.sendRequest(request);//...省略其他逻辑...}
}// 里式替换原则
Demo demo = new Demo();
demo.demofunction(new SecurityTransporter(/*省略参数*/););

在上面的代码中,子类 SecurityTransporter 的设计完全符合里式替换原则,可以替换父类出现的任何位置,并且原来代码的逻辑行为不变且正确性也没有被破坏。

这不就是多态么?

从刚刚的例子和定义描述来看,里式替换原则跟多态看起来确实有点类似,但实际上它们完全是两回事。为什么这么说呢?

我们还是通过刚才这个例子来解释一下。不过,我们需要对 SecurityTransporter 类中 sendRequest() 函数稍加改造一下。改造前,如果 appId 或者 appToken 没有设置,我们就不做校验;改造后,如果 appId 或者 appToken 没有设置,则直接抛出 NoAuthorizationRuntimeException 未授权异常。改造前后的代码对比如下所示:

// 改造前:
public class SecurityTransporter extends Transporter {//...省略其他代码..@Overridepublic Response sendRequest(Request request) {if (StringUtils.isNotBlank(appId) && StringUtils.isNotBlank(appToken)) {request.addPayload("app-id", appId);request.addPayload("app-token", appToken);}return super.sendRequest(request);}
}// 改造后:
public class SecurityTransporter extends Transporter {//...省略其他代码..@Overridepublic Response sendRequest(Request request) {if (StringUtils.isBlank(appId) || StringUtils.isBlank(appToken)) {throw new NoAuthorizationRuntimeException(...);}request.addPayload("app-id", appId);request.addPayload("app-token", appToken);return super.sendRequest(request);}
}

在改造之后的代码中,如果传递进 demoFunction() 函数的是父类 Transporter 对象,那 demoFunction() 函数并不会有异常抛出,但如果传递给 demoFunction() 函数的是子类 SecurityTransporter 对象,那 demoFunction() 有可能会有异常抛出。尽管代码中抛出的是运行时异常(Runtime Exception),我们可以不在代码中显式地捕获处理,但子类替换父类传递进 demoFunction 函数之后,整个程序的逻辑行为有了改变。

虽然改造之后的代码仍然可以通过 Java 的多态语法,动态地用子类 SecurityTransporter 来替换父类 Transporter,也并不会导致程序编译或者运行报错。但是,从设计思路上来讲,SecurityTransporter 的设计是不符合里式替换原则的。

多态是面向对象编程的一大特性,也是面向对象编程语言的一种语法。它是一种代码实现的思路。而里式替换是一种设计原则,是用来指导继承关系中子类该如何设计的,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑以及不破坏原有程序的正确性。

哪些代码明显违背了 LSP?

实际上,里式替换原则还有另外一个更加能落地、更有指导意义的描述,那就是“Design By Contract”,中文翻译就是“按照协议来设计”。

看起来比较抽象,我来进一步解读一下。子类在设计的时候,要遵守父类的行为约定(或者叫协议)。父类定义了函数的行为约定,那子类可以改变函数的内部实现逻辑,但不能改变函数原有的行为约定。这里的行为约定包括:函数声明要实现的功能;对输入、输出、异常的约定;甚至包括注释中所罗列的任何特殊说明。实际上,定义中父类和子类之间的关系,也可以替换成接口和实现类之间的关系。

1. 子类违背父类声明要实现的功能

父类中提供的 sortOrdersByAmount() 订单排序函数,是按照金额从小到大来给订单排序的,而子类重写这个 sortOrdersByAmount() 订单排序函数之后,是按照创建日期来给订单排序的。那子类的设计就违背里式替换原则。

2. 子类违背父类对输入、输出、异常的约定

在父类中,某个函数约定:运行出错的时候返回 null;获取数据为空的时候返回空集合(empty collection)。而子类重载函数之后,实现变了,运行出错返回异常(exception),获取不到数据返回 null。那子类的设计就违背里式替换原则。

在父类中,某个函数约定,输入数据可以是任意整数,但子类实现的时候,只允许输入数据是正整数,负数就抛出,也就是说,子类对输入的数据的校验比父类更加严格,那子类的设计就违背了里式替换原则。

在父类中,某个函数约定,只会抛出 ArgumentNullException 异常,那子类的设计实现中只允许抛出 ArgumentNullException 异常,任何其他异常的抛出,都会导致子类违背里式替换原则。

3. 子类违背父类注释中所罗列的任何特殊说明

父类中定义的 withdraw() 提现函数的注释是这么写的:“用户的提现金额不得超过账户余额……”,而子类重写 withdraw() 函数之后,针对 VIP 账号实现了透支提现的功能,也就是提现金额可以大于账户余额,那这个子类的设计也是不符合里式替换原则的。

实习成长之路——设计原则三:里式替换(LSP)跟多态有何区别?哪些代码违背了LSP?相关推荐

  1. 设计原则:里式替换原则(LSP)

    系列文章 设计原则:单一职责(SRP) 设计原则:开闭原则(OCP) 设计原则:里式替换原则(LSP) 设计原则:接口隔离原则(ISP) 设计原则:依赖倒置原则(DIP) 何谓高质量代码? 理解RES ...

  2. 设计模式-02.经典设计原则-第一节-单一职责原则,开闭原则,里式替换,接口隔离【万字长文系列】

    文章目录 设计模式经典设计原则-第一节 单一职责原则(SRP) 如何理解单一职责原则? 如何判断类的职责是否足够单一? 类的职责是否设计得越单一越好? 开闭原则(OCP) 如何理解"对扩展开 ...

  3. 里式替换(LSP)跟多态有何区别?

    在上两节课中,我们学习了 SOLID 原则中的单一职责原则和开闭原则,这两个原则都比较重要,想要灵活应用也比较难,需要你在实践中多加练习.多加体会.今天,我们再来学习 SOLID 中的"L& ...

  4. LSP 里式替换原则 c# 1614092345

    LSP 里式替换原则 子类对象可以替换所有使用的父类对象 且程序行为没有变化 简单的说: 父类的方法,子类都可以使用 直接使用子类,可以同时获取父类的方法与自有的方法,应用的泛围更广

  5. 论重写和里式替换原则(LSP)

    对于重写的原则,很多人总是巴拉巴拉一大堆两同两小一大,记不住不说,还不明白为啥,搞得花里胡哨. 其实万事万物的结果自然有其原理,JAVA作为一门编程语言,其更是有严格的语言规范和简洁性要求. 那为啥重 ...

  6. 代码精进之路-设计原则

    设计原则是前辈的总结,为后来人提供经验,写出更好的代码,降低系统复杂度,提高代码的稳定性,可维护性. 有时候你觉得这个方案这样设计也可以,那样设计也没问题,犹豫不决,这时不妨参考下设计原则,也许你心中 ...

  7. python大神的成长之路_Python大神成长之路: 第三次学习记录 集合 函数 装饰 re...

    学习记录day03 字符串可以直接切片,But字符串不可修改 字符串修改:生成了一个新的字符串 LIst修改,在原基础上修改(原内存上) 集合是一个无序的,不重复的数据组合,它的主要作用如下: 去重, ...

  8. C与C++成长之路——c提高三之多级指针

    一.const 的使用 //const修饰一个变量为只读const int a = 10;//a = 100; //error//指针变量,指针指向的内存,2个不同的概念char buff[] = & ...

  9. 破茧成蝶之用户体验设计师的成长之路-设计实施

    流程:需求分析-->设计规划-->设计实施-->开发测试 设计规范 交互规范先于视觉规范 从大到小:现制定大的设计方向,在制定项目中单个详细的说明 设计评审 明确设计方向 阐述设计分 ...

  10. 成长之路:Docker(三)使用

    Hello World Docker允许在容器内运行应用程序. docker run:在容器内运行一个应用程序. [root@localhost ~]# docker run ubuntu:15.10 ...

最新文章

  1. phpexecl保存mysql_【PHP】将数据库表保存为Excel(PHPExcel)
  2. 并发的HashMap为什么会引起死循环?(转)
  3. vuejs和php的区别,VueJS全面解析
  4. 使用 Equinox 开发 OSGi 应用程序
  5. .Net Core微服务架构技术栈的那些事
  6. 云技术-SaaS架构初步理解
  7. Javascript笔记:(实践篇)从jQuery插件技术说起-分析extend方法的源码(发现extend方法里有bug)(下篇)...
  8. python matplotlib 保存图片失真_Python matplotlib线框失真
  9. eclipse启动时return code 13
  10. json-viewer.js案例
  11. Activiti学习之根据条件判断流程走向
  12. Vulkan同步机制和图形-计算-图形转换的风险(一)
  13. ptp4l linux,如何使用PTP4l测试PTPV2协议精度?
  14. 创建简易的金融数据库
  15. 百度直连+cns完美,那怎么搭建CNS配合百度直连呢
  16. linux nand 驱动,Linux NAND FLASH驱动分析(一)
  17. CC2530模块进行ZigBee实验小结
  18. Python自动化开发从浅入深-进阶(script,dom,jquery 基础)
  19. IIS + PHP 配置
  20. 质性分析软件nvivo的学习(二)

热门文章

  1. pythonsqlite导出json_使用python将mysql数据库的数据转换为json数据
  2. windows 查看_解决 Windows 照片查看器无法显示图片问题
  3. CSS:transform
  4. 零拷贝的基本原理及使用Java通过零拷贝实现数据传输
  5. Fiddler—PC上实现手机的抓包
  6. My97DatePicker JS时间控件 当前日期前后不能选
  7. C++_智能指针shared_ptr、unique_ptr、weak_ptr、auto_ptr总结
  8. GMapping源码分析之随手笔记
  9. centos挂载ntfs文件系统
  10. kill -3 获取threaddump信息