当前位置: 首页 > news >正文

【设计模式之美】SOLID 原则之三:里式替换(LSP)跟多态有何区别?如何理解LSP中子类遵守父类的约定

文章目录

    • 一. 如何理解“里式替换原则”?
    • 二. 哪些代码明显违背了 LSP?
    • 三. 回顾

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

子类对象能够替换程序中父类对象出现的任何地方,并且保证原来程序的逻辑行为不变及正确性不被破坏。

里氏替换原则例子如下:

//>1. 父类 Transporter 使用 HttpClient 来传输网络数据。
//>2. 子类 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(/*省略参数*/););

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

先改造下程序(改造成多态)

//改造前,如果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);}
}

从设计思路上来讲,SecurityTransporter 的设计是不符合里式替换原则的,因为它改变了父类原有的规则,我们接下来讨论里氏替换中协议的具体含义。

 

二. 哪些代码明显违背了 LSP?

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

具体说明一下

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

 

如下几个违反里式替换原则的例子,来说明约定的含义:

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

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

 

2.子类违背父类对输入、输出、异常的约定,那子类的设计就违背里式替换原则,如下:

  • 在父类中运行出错的时候返回 null;获取数据为空的时候返回空集合(empty collection)。而子类重载函数之后,实现变了,运行出错返回异常(exception),获取不到数据返回 null。
  • 在父类中,某个函数约定,输入数据可以是任意整数,但子类实现的时候,只允许输入数据是正整数,负数就抛出,也就是说,子类对输入的数据的校验比父类更加严格
  • 在父类中,某个函数约定,只会抛出 ArgumentNullException 异常,那子类的设计实现中只允许抛出 ArgumentNullException 异常,任何其他异常的抛出。

 

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

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

 

三. 回顾

里式替换原则是用来指导,继承关系中子类该如何设计的一个原则。


  • 理解里式替换原则,最核心的就是理解“design by contract,按照协议来设计”这几个字。父类定义了函数的“约定”(或者叫协议),那子类可以改变函数的内部实现逻辑,但不能改变函数原有的“约定”。
  • 这里的约定包括:函数声明要实现的功能;对输入、输出、异常的约定;甚至包括注释中所罗列的任何特殊说明。

 

参考:《设计模式之美》–王争

http://www.lryc.cn/news/281430.html

相关文章:

  • 代码随想录第六十三天——被围绕的区域,太平洋大西洋水流问题,最大人工岛
  • Docker 项目如何使用 Dockerfile 构建镜像?
  • 实践学习PaddleScience飞桨科学工具包
  • Vue 中修改 Element 组件的 下拉菜单(Dropdown) 的样式
  • 达梦数据库主备集群
  • Spark Doris Connector 可以支持通过 Spark 读取 Doris 数据类型不兼容报错解决
  • 深入理解 go chan
  • java+vue基于Spring Boot的渔船出海及海货统计系统
  • Linux第25步_在虚拟机中备份“ST官方的TF-A源码”
  • 统计学-R语言-4.1
  • C++(1) —— 基础语法入门
  • 构建基于RHEL8系列(CentOS8,AlmaLinux8,RockyLinux8等)的支持63个常见模块的PHP8.1.20的RPM包
  • Vue-插槽(Slots)
  • 新火种AI|GPT-5前瞻!GPT-5将具备哪些新能力?
  • 安防视频监控系统EasyCVR设备分组中在线/离线数量统计的开发与实现
  • spring cloud之集成sentinel
  • 让车辆做到“耳听八方”,毫米波雷达芯片与系统设计
  • Python如何实现数据驱动的接口自动化测试
  • 高级分布式系统-第15讲 分布式机器学习--联邦学习
  • 小程序基础学习(事件处理)
  • 网络协议与攻击模拟_01winshark工具简介
  • 【linux学习笔记】网络
  • JUC-线程中断机制和LockSupport
  • 哈希表与哈希算法(Python系列30)
  • 『 C++ 』AVL树详解 ( 万字 )
  • Python下载安装pip方法与步骤_pip国内镜像
  • 自动化测试框架pytest系列之基础概念介绍(一)
  • 编码技巧:如何在Golang中高效解析和生成XML
  • 24校招,帆书测试开发工程师一面
  • Java 方法以及在计算机内部的调用问题