文化传媒网站封面/找合作项目app平台
前言
上一篇,我们讲了开放封闭原则,想要让系统符合开放封闭原则,最重要的就是我们要构建起相应的扩展模型,所以,我们要面向接口编程。
而大部分的面向接口编程要依赖于继承实现,继承的重要性不如封装和多态,但在大部分面向对象程序设计语言中,继承却是构建一个对象体系的重要组成部分。
理论上,在定义了接口之后,我们就可以把继承这个接口的类完美地嵌入到我们设计好的体系之中。然而,用了继承,子类就一定设计对了吗?事情可能并没有这么简单。新的类虽然在语法上声明了一个接口,形成了一个继承关系,但我们要想让这个子类真正地扮演起这个接口的角色,还需要有一个好的继承指导原则。
所以,这一篇,我们就来看看可以把继承体系设计好的设计原则: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监听端口 二、配置网…...

Ansible基础4——变量、机密、事实
文章目录 一、变量二、机密2.1 创建加密文件2.2 查看加密文件2.3 编辑加密文件内容2.4 加密现有文件2.5 解密文件2.6 更改加密密码 三、事实3.1 收集展示事实3.2 展示某个结果3.3 新旧事实命令3.4 关闭事实3.5 魔法变量 一、变量 常设置的变量: 要创建的用户要安装的…...

React实现Vue的watch监听属性
在 Vue 中可以简单地使用 watch 来监听数据的变化,还能获取到改变前的旧值,而在 React 中是没有 watch 的。 React中比较复杂,但是我们如果想在 React 中实现一个类似 Vue 的 watch 监听属性,也不是没有办法。 在React类组件中实…...

axios、跨域与JSONP、防抖和节流
文章目录 一、axios1、什么是axios2、axios发起GET请求3、axios发起POST请求4、直接使用axios发起请求 二、跨域与JSONP1、了解同源策略和跨域2、JSONP(1)实现一个简单的JSONP(2)JSONP的缺点(3)jQuery中的J…...

macOS Ventura 13.5beta2 (22G5038d)发布
系统介绍 黑果魏叔 6 月 1 日消息,苹果今日向 Mac 电脑用户推送了 macOS 13.5 开发者预览版 Beta 2 更新(内部版本号:22G5038d),本次更新距离上次发布隔了 12 天。 macOS Ventura 带来了台前调度、连续互通相机、Fac…...

jwt----介绍,原理
token:服务的生成的加密字符串,如果存在客户端浏览器上,就叫cookie -三部分:头,荷载,签名 -签发:登录成功,签发 -认证:认证类中认证 # jwt&…...

Three.js--》实现3d水晶小熊模型搭建
目录 项目搭建 初始化three.js基础代码 加载背景纹理 加载小熊模型 今天简单实现一个three.js的小Demo,加强自己对three知识的掌握与学习,只有在项目中才能灵活将所学知识运用起来,话不多说直接开始。 项目搭建 本案例还是借助框架书写…...

《阿里大数据之路》研读笔记(1)
首先先看到OLAP和OLTP的区别: OLTP(Online transaction processing):在线/联机事务处理。典型的OLTP类操作都比较简单,主要是对数据库中的数据进行增删改查,操作主体一般是产品的用户或者是操作人员。 OLAP(Online analytical processing):…...

Logback 日志框架详解
一、Logback 简介 Logback 是一个日志框架,旨在成为 log4j 的替代品。它由 Ceki Glc 创建并维护,是一款开源的日志框架,是 slf4j(Simple Logging Facade for Java)的实现。相比于 log4j,Logback 具有更高的…...

BIO、NIO、AIO 有什么区别?
BIO (Blocking I/O): Block IO 同步阻塞式 IO ,传统 IO,特点是模式简单、使用方便,并发处理能力低。 同步阻塞 I/O 模式,数据的读取写入必须阻塞在一个线程内等待其完成,在活动连接数不是特别高(…...

nginx和tomcat负载均衡、静态分离
tomcat重要目录 bin 存放启动和关闭Tomcat脚本conf存放Tomcat不同的配置文件doc存放Tomcat文档lib存放Tomcat运行需要的库文件logs存放Tomcat执行时的log文件src存放Tomcat的源代码webappsTomcat的主要Web发布目录work存放jsp编译后产生的class文件 nginx负载均衡原理 nginx实…...