Liskov替换原则:用了继承,子类就设计对了吗?
前言
上一篇,我们讲了开放封闭原则,想要让系统符合开放封闭原则,最重要的就是我们要构建起相应的扩展模型,所以,我们要面向接口编程。
而大部分的面向接口编程要依赖于继承实现,继承的重要性不如封装和多态,但在大部分面向对象程序设计语言中,继承却是构建一个对象体系的重要组成部分。
理论上,在定义了接口之后,我们就可以把继承这个接口的类完美地嵌入到我们设计好的体系之中。然而,用了继承,子类就一定设计对了吗?事情可能并没有这么简单。新的类虽然在语法上声明了一个接口,形成了一个继承关系,但我们要想让这个子类真正地扮演起这个接口的角色,还需要有一个好的继承指导原则。
所以,这一篇,我们就来看看可以把继承体系设计好的设计原则:Liskov 替换法则。
Liskov 替换原则
2008 年,图灵奖授予 Barbara Liskov,表彰她在程序设计语言和系统设计方法方面的卓越工作。她在设计领域影响最深远的就是以她名字命名的 Liskov 替换原则(Liskov substitution principle,简称 LSP)。
1988 年,Barbara Liskov 在描述如何定义子类型时写下这样一段话:
这里需要如下替换性质:若每个类型 S 的对象 o1,都存在一个类型 T 的对象 o2,使得在所有针对 T 编程的程序 P 中,用 o1 替换 o2 后,程序 P 行为保持不变,则 S 是 T 的子类型。
用通俗的讲法来说,意思就是,子类型(subtype)必须能够替换其父类型(base type)。
这句话看似简单,但是违反这个原则,后果是很严重的,比如,父类型规定接口不能抛出异常,而子类型抛出了异常,就会导致程序运行的失败。
虽然很好理解,但你可能会有个疑问,我的子类型不都是继承自父类型,咋就能违反 LSP 呢?这个 LSP 是不是有点多此一举呢?
我们来看个例子,有不少的人经常写出类似下面这样的代码:
void handle(final Handler handler) {if (handler instanceof ReportHandler) {// 生成报告((ReportHandler)handler).report();return;}if (handler instanceof NotificationHandler) {// 发送通知((NotificationHandler)handler).sendNotification();}...
}
根据上一篇的内容,这段代码显然是违反了 OCP 的。另外,在这个例子里面,虽然我们定义了一个父类型 Handler,但在这段代码的处理中,是通过运行时类型识别(Run-Time Type Identification,简称 RTTI),也就是这里的 instanceof,知道子类型是什么的,然后去做相应的业务处理。
但是,ReportHandler 和 NotificationHandler 虽然都是 Handler 的子类,但它们没有统一的处理接口,所以,它们之间并不存在一个可以替换的关系,这段代码也是违反 LSP 的。这里我们就得到了一个经验法则,如果你发现了任何做运行时类型识别的代码,很有可能已经破坏了 LSP。
再来看一个实例,也是违法了LSP
public class TestA {public void fun(int a,int b){System.out.println(a+"+"+b+"="+(a+b));}public static void main(String[] args) {System.out.println("父类的运行结果");TestA a=new TestA();a.fun(1,2);//父类存在的地方,可以用子类替代//子类B替代父类ASystem.out.println("子类替代父类后的运行结果");TestB b=new TestB();b.fun(1,2);}
}
class TestB extends TestA{@Overridepublic void fun(int a, int b) {System.out.println(a+"-"+b+"="+(a-b));}
}
大家肯定也都能猜出来结果是什么样子的:
父类的运行结果
1+2=3
子类替代父类后的运行结果
1-2=-1Process finished with exit code 0
我们想要的结果是“1+2=3”。可以看到,方法重写后结果就不是了我们想要的结果了,也就是这个程序中子类B不能替代父类A。这违反了里氏替换原则原则,从而给程序造成了错误。
子类中可以增加自己特有的方法
public class TestA {public void fun(int a,int b){System.out.println(a+"+"+b+"="+(a+b));}public static void main(String[] args) {System.out.println("父类的运行结果");TestA a=new TestA();a.fun(1,2);//父类存在的地方,可以用子类替代//子类B替代父类ASystem.out.println("子类替代父类后的运行结果");TestB b=new TestB();b.fun(1,2);b.newFun();}
}
class TestB extends TestA{public void newFun(){System.out.println("这是子类的新方法...");}
}
这次运行出来的代码结果就是我们意料中的内容:
父类的运行结果
1+2=3
子类替代父类后的运行结果
1+2=3
这是子类的新方法...Process finished with exit code 0
基于行为的 IS-A
如果你去阅读关于 LSP 的资料,很有可能会遇到一个有趣的问题,也就是长方形正方形问题。在我们对于几何通常的理解中,正方形是一种特殊的长方形。所以,我们可能会写出这样的代码:
class Rectangle {private int height;private int width;// 设置长度public void setHeight(int height) {this.height = height;}// 设置宽度public void setWidth(int width) {this.width = width;}//面积public int area() {return this.height * this.width;}
}class Square extends Rectangle {// 设置边长public void setSide(int side) {this.setHeight(side);this.setWidth(side);}@Overridepublic void setHeight(int height) {this.setSide(height);}@Overridepublic void setWidth(int width) {this.setSide(width);}
}
这段代码看上去一切都很好,然而,它却是有问题的,因为它在下面这个测试里会失败:
import org.junit.Assert;import static org.hamcrest.CoreMatchers.is;public class Test {public static void main(String[] args) {Rectangle rect = new Square();rect.setHeight(4); // 设置长度rect.setWidth(5); // 设置宽度Assert.assertThat(rect.area(), is(20));//对结果进行断言}
}
如果想保证断言(assert)的正确性,Rectangle 和 Square 二者在这里是不能互相替换的。使用 Rectangle 的代码必须知道自己使用的到底是 Rectangle 还是 Square。
出现这个问题的原因就在于,我们构建模型时,会理所当然地把我们直觉中的模型直接映射到代码模型上。在我们直觉中,正方形确实是一种长方形。
在我们设计的这个对象体系中,边长是可以调整的。然而,在几何的体系里面,长方形的边长是不能随意改变的,设置好了就是设置好了。换句话说,两个体系内,“长方形”的行为是不一致的。所以,在这个对象体系中,正方形边长即使可以调整,但正方形也并不是一个长方形,也就是说,它们之间不满足 IS-A 关系。
你可能听说过继承要符合 IS-A 的关系,也就是说,如果 A 是 B 的子类,就需要满足 A 是一个 B(A is a B)。但你有没有想过,凭什么 A 是一个 B 呢?判断依据从何而来呢?你应该知道,这种判定显然不能依靠直觉。其实,从前面的分析中,你也能看出一些端倪来,IS-A 的判定是基于行为的,只有行为相同,才能说是满足 IS-A 的关系。
更广泛的 LSP
如果理解了 LSP,你会发现,它不仅适用于类级别的设计,还适用于更广泛的接口设计。比如,我们在开发中经常会遇到系统集成的问题,有不同的厂商都要通过 REST 接口把他们的统计信息上报到你的系统中,但是,有一个大厂上报的消息格式没法遵循你定义的格式,因为他的系统改动起来难度比较大。你该怎么办呢?
也许,专门为大厂设计一个特定接口是最简单的想法,但是,一旦开了这个口子,后面的各种集成接口都要为这个大厂开发一份特殊的,而且,如果未来再有其他大厂也提出要求,你要不要为它们也设计特殊接口呢?事实上,很多项目功能不多,但接口特别多,就是因为在这种决策的时候开了口子。请记住,公开接口是最宝贵的资源,千万不能随意添加。
如果我们用 LSP 的角度看这个问题,通用接口就是一个父类接口,而不同厂商的内容就相当于一个个子类。让厂商面对特定接口,系统将变得无法维护。后期随着人员变动,接口只会更加膨胀,到最后,没有人说清楚每个接口到底是做什么的。
好,那我们决定采用统一的接口,可是不同的消息格式该怎么处理呢?首先,我们需要区分出不同的厂商,办法有很多,无论是通过 REST 的路径,还是 HTTP 头的方式,我们可以得到一个标识符。然后呢?
很容易想到的做法就是写出一个 if 语句来,像下面这样:
if (identfier.equals("SUPER_VENDOR")) {...
}
但是,千万要遏制自己写 if 的念头,一旦开了这个头,后续的代码也将变得难以维护。我们可以做的是,提供一个解析器的接口,根据标识符找到一个对应的解析器,像下面这样:
RequestParser parser = parsers.get(identifier);
if (parser != null) {return parser.parse(request);
}
这样一来,即便有其他厂商再因为某些奇怪的原因要求有特定的格式,我们要做的只是提供一个新的接口实现。这样一来,所有代码的行为就保持了一致性,核心的代码结构也保持了稳定。
总结
- Liskov 替换原则,其主要意思是说子类型必须能够替换其父类型。
- 理解 LSP,我们需要站在父类的角度去看,而站在子类的角度,常常是破坏 LSP 的做法,一个值得警惕的现象是,代码中出现 RTTI 相关的代码。
- 继承需要满足 IS-A 的关系,但 IS-A 的关键在于行为上的一致性,而不能单纯凭日常的概念或直觉去理解。
- LSP 不仅仅可以用在类关系的设计上,我们还可以把它用在更广泛的接口设计中。任何接口都是宝贵的,在设计时,都要精心考量。
- LSP 的根基在于继承,但显然接口继承才是重点。
相关文章:
Liskov替换原则:用了继承,子类就设计对了吗?
前言 上一篇,我们讲了开放封闭原则,想要让系统符合开放封闭原则,最重要的就是我们要构建起相应的扩展模型,所以,我们要面向接口编程。 而大部分的面向接口编程要依赖于继承实现,继承的重要性不如封装和多…...
腾讯云服务器SA3实例AMD处理器CPU网络带宽性能详解
腾讯云AMD服务器SA3实例CPU采用2.55GHz主频的AMD EPYCTM Milan处理器,睿频3.5GHz,搭载最新一代八通道DDR4,内存计算性能稳定,默认网络优化,最高内网收发能力达1900万pps,最高内网带宽可支持100Gbps。腾讯云…...
接口测试常用测试点
接口测试是测试系统组件间接口的一种测试。接口测试主要用于检测外部系统与系统之间以及内部各个子系统之间的交互点。测试的重点是要检查数据的交换,传递和控制管理过程,以及系统间的相互逻辑依赖关系等。 测试的策略: 接口测试也是属于功…...
Unity之OpenXR+XR Interaction Toolkit接入HTC Vive解决手柄无法使用的问题
前言 随着Unity版本的不断进化,VR的接口逐渐统一,现在大部分的VR项目都开始使用OpenXR开发了。基于OpenXR,我们可以快速适配HTC,Pico,Oculus,等等设备。 今天我们要说的问题就是,当我们按照官方的标准流程配置完OpenXR后(参考:Unity之OpenXR+XR Interaction Toolkit…...
AC变DC220V变5V小家电电源芯片-AH8652、AH8669
Q: 什么是AH8652和AH8669电源芯片? A: AH8652和AH8669都是AC变DC的电源芯片,适用于将输入的交流电压(220V)转换为5V直流电压输出,用于小家电的电源模块等应用。 AC变DC220V变5V小家电电源芯片-AH8669 Q: AH8652和AH8669的最大输…...
深度学习笔记之循环神经网络(九)GRU的反向传播过程
深度学习笔记之循环神经网络——GRU的反向传播过程 引言回顾: GRU \text{GRU} GRU的前馈计算过程场景设计 反向传播过程 T \mathcal T T时刻的反向传播过程 T − 1 \mathcal T - 1 T−1时刻的反向传播路径 T − 2 \mathcal T - 2 T−2时刻的反向传播路径 总结 引言 …...
ISFP型人格的性格缺陷和心理问题分析
ISFP人格的特征:性格敏感、为人善良、是具有有创造力的人格类型。他们喜欢追求内心的感受和情感,注重自由、个性和独立。ISFP性人格偏于内向,善于自省,对情绪敏感度高,同理心强。 每种人格类型的都有各自的优势和不足…...
HTML <dir> 标签
HTML5 中不支持 <dir> 标签在 HTML 4 中用于列出目录标题。 实例 目录列表: <dir><li>HTML</li><li>XHTML</li><li>CSS</li> </dir>浏览器支持 IEFirefoxChromeSafariOpera 所有主流浏览器都支持 <…...
leetcode 621. 任务调度器
题目链接:leetcode 621 1.题目 给你一个用字符数组 tasks 表示的 CPU 需要执行的任务列表。其中每个字母表示一种不同种类的任务。任务可以以任意顺序执行,并且每个任务都可以在 1 个单位时间内执行完。在任何一个单位时间,CPU 可以完成一个…...
线程任务的取消
如果外部代码能在某个操作正常完成之前将其置入“完成”状态,那么这个操作就可以称为可取消的(Cancellable)。取消某个操作的原因很多: 用户请求取消。用户点击图形界面程序中的“取消”按钮,或者通过管理接口来发出取消请求,例如JMX (Java …...
在线聊天项目
人事管理项目-在线聊天 后端接口实现前端实现 在线聊天是一个为了方便HR进行快速沟通提高工作效率而开发的功能,考虑到一个公司中的HR并不多,并发量不大,因此这里直接使用最基本的WebSocket来完成该功能。 后端接口实现 要使用WebSocket&…...
动态规划-硬币排成线
动态规划-硬币排成线 1 描述2 样例2.1 样例 1:2.2 样例 2:2.3 样例 3: 3 算法解题思路及实现3.1 算法解题分析3.1.1 确定状态3.1.2 转移方程3.1.3 初始条件和边界情况3.1.4 计算顺序 3.2 算法实现3.2.1 动态规划常规实现3.2.2 动态规划滚动数组 该题是lintcode的第394题&#x…...
有效的括号——力扣20
题目描述 思路 1.判断括号的有效性可以使用「栈」这一数据结构来解决 2.遍历给定的字符串 s。当遇到一个左括号时,我们会期望在后续的遍历中,有一个相同类型的右括号将其闭合。由于后遇到的左括号要先闭合,因此我们可以将这个左括号放入栈顶。…...
【轻量级网络】华为诺亚:VanillaNet
文章目录 0. 前言1. 网络结构2. VanillaNet非线性表达能力增强策略2.1 深度训练2.2 扩展激活函数 3. 总结4. 参考 0. 前言 随着人工智能芯片的发展,神经网络推理速度的瓶颈不再是FLOPs或参数量,因为现代GPU可以很容易地进行计算能力较强的并行计算。相比…...
读写ini配置文件(C++)
文章目录 1、为什么要使用ini或者其它(例如xml,json)配置文件?2、ini文件基本介绍3、ini配置文件的格式4、C读写ini配置文件5、 代码示例6、 配置文件的解析库 文章转载于:https://blog.csdn.net/weixin_44517656/article/details/109014236 1、为什么要…...
Python对接亚马逊电商平台SP-API的一些概念理解准备
❝ 除了第三方服务商,其实亚马逊卖家本身也可以通过和SP-API的对接,利用程序来自动化亚马逊店铺销售运营管理中很多环节的工作,简单的应用比如可以利用SP-API的对接,实现亚马逊卖家后台各类报表的定期自动下载以及数据分析整理工…...
[Halcon3D] 主流的3D光学视觉方案及原理
📢博客主页:https://loewen.blog.csdn.net📢欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正!📢本文由 丶布布原创,首发于 CSDN,转载注明出处🙉📢现…...
Go Web下gin框架使用(二)
〇、gin 路由 Gin是一个用于构建Web应用程序的Go语言框架,它具有简单、快速、灵活的特点。在Gin中,可以使用路由来定义URL和处理程序之间的映射关系。 r : gin.Default()// 访问 /index 这个路由// 获取信息r.GET("/index", func(c *gin.Con…...
算法笔记-线段树合并
线段树合并 前置知识:权值线段树、动态开点 将两棵线段树的信息合并成一棵线段树。 可以新建一颗线段树保存原来两颗线段树的信息,也可以将第二棵线段树维护的信息加到第一棵线段树上。 前者的空间复杂度较高,如果合并之前的线段树不会再用…...
Fiddler抓取IOS数据包实践教程
Fiddler是一个http协议调试代理工具,它能够记录并检查所有你的电脑和互联网之间的http通讯,设置断点,查看所有的“进出”Fiddler的数据(指cookie,html,js,css等文件)。 本章教程,主要介绍如何利用Fiddler抓取IOS数据包相关教程。 目录 一、打开Fiddler监听端口 二、配置网…...
19c补丁后oracle属主变化,导致不能识别磁盘组
补丁后服务器重启,数据库再次无法启动 ORA01017: invalid username/password; logon denied Oracle 19c 在打上 19.23 或以上补丁版本后,存在与用户组权限相关的问题。具体表现为,Oracle 实例的运行用户(oracle)和集…...
Flask RESTful 示例
目录 1. 环境准备2. 安装依赖3. 修改main.py4. 运行应用5. API使用示例获取所有任务获取单个任务创建新任务更新任务删除任务 中文乱码问题: 下面创建一个简单的Flask RESTful API示例。首先,我们需要创建环境,安装必要的依赖,然后…...
突破不可导策略的训练难题:零阶优化与强化学习的深度嵌合
强化学习(Reinforcement Learning, RL)是工业领域智能控制的重要方法。它的基本原理是将最优控制问题建模为马尔可夫决策过程,然后使用强化学习的Actor-Critic机制(中文译作“知行互动”机制),逐步迭代求解…...
遍历 Map 类型集合的方法汇总
1 方法一 先用方法 keySet() 获取集合中的所有键。再通过 gey(key) 方法用对应键获取值 import java.util.HashMap; import java.util.Set;public class Test {public static void main(String[] args) {HashMap hashMap new HashMap();hashMap.put("语文",99);has…...
多模态商品数据接口:融合图像、语音与文字的下一代商品详情体验
一、多模态商品数据接口的技术架构 (一)多模态数据融合引擎 跨模态语义对齐 通过Transformer架构实现图像、语音、文字的语义关联。例如,当用户上传一张“蓝色连衣裙”的图片时,接口可自动提取图像中的颜色(RGB值&…...
自然语言处理——循环神经网络
自然语言处理——循环神经网络 循环神经网络应用到基于机器学习的自然语言处理任务序列到类别同步的序列到序列模式异步的序列到序列模式 参数学习和长程依赖问题基于门控的循环神经网络门控循环单元(GRU)长短期记忆神经网络(LSTM)…...
安宝特方案丨船舶智造的“AR+AI+作业标准化管理解决方案”(装配)
船舶制造装配管理现状:装配工作依赖人工经验,装配工人凭借长期实践积累的操作技巧完成零部件组装。企业通常制定了装配作业指导书,但在实际执行中,工人对指导书的理解和遵循程度参差不齐。 船舶装配过程中的挑战与需求 挑战 (1…...
[ACTF2020 新生赛]Include 1(php://filter伪协议)
题目 做法 启动靶机,点进去 点进去 查看URL,有 ?fileflag.php说明存在文件包含,原理是php://filter 协议 当它与包含函数结合时,php://filter流会被当作php文件执行。 用php://filter加编码,能让PHP把文件内容…...
MySQL 主从同步异常处理
阅读原文:https://www.xiaozaoshu.top/articles/mysql-m-s-update-pk MySQL 做双主,遇到的这个错误: Could not execute Update_rows event on table ... Error_code: 1032是 MySQL 主从复制时的经典错误之一,通常表示ÿ…...
保姆级【快数学会Android端“动画“】+ 实现补间动画和逐帧动画!!!
目录 补间动画 1.创建资源文件夹 2.设置文件夹类型 3.创建.xml文件 4.样式设计 5.动画设置 6.动画的实现 内容拓展 7.在原基础上继续添加.xml文件 8.xml代码编写 (1)rotate_anim (2)scale_anim (3)translate_anim 9.MainActivity.java代码汇总 10.效果展示 逐帧…...
