设计原则-里氏替换原则

news2024/11/26 10:30:02

凡事皆有利弊,面向对象设计语言通过提供继承、多态等机制使得项目代码更具有复用性、可扩展性等优点,但是这些机制也存在一定的使用风险,比如继承的滥用、多态实现的不确定性等问题都有可能会引起重大线上事故。

一、里氏替换原则概念

里氏替换原则由Barbara Liskov发表是在1994年一篇学术论文《A behavioral notion of subtyping》。这篇论文主要讨论了继承机制下基类与子类的关系,文中认为子类型关系(继承)应保持子类型对象的行为特征与基类的行为特征相同,而仅非结构特征,即子类对象应该能够替代基类对象并保持程序的正确性。这就是历史替换原则,用更加非学术的语言定义可以简述为:所有引用基类的地方必须能够显示替换为其子类对象
里氏替换原则讲述的是在使用继承来处理类之间关系时遵循的约束,若不遵从该原则就可能会使得程序出现异常或错误。在定义中,我们需要先来理解下"(可)替换"这个词。(可)替换一方面是指编译不会报错,这就意味着子类必须拥有基类的所有方法;另外一方面是指运行不会报错。前面说了里氏替换原则要求子类的行为特征与基类相同,因此子类的方法的业务含义必须与基类保持一致或兼容。满足了这两点,我才认为子类对于基类时可替换的,即满足里氏替换原则。
【总结】里氏替换原则对继承使用提出了两个约束:

  1. 子类必须拥有基类的所有方法
  2. 子类继承的方法业务含义必须与基类保持一致或兼容

第1个约束我们十分容易理解,不满足编译自然会报错。第二个约束中我们下节展开叙述。

二、子基类行为特征一致

前面他们提到里氏替换原则要求子类继承并实现基类方法必须保持一致或兼容(学术话语即使行为特征兼容),本节将会通过示例来阐述兼容含义与意义。
假设你所在的公司接到一个汽车制造业务相关需求,需求的其中一部分是实现汽车的加油方法内容。初期设计方案如下:
在这里插入图片描述
如上类图所示,类GasolineCar继承了类Car,Car中定义了表示给汽车补充燃油的方法。此时Client方法内容为:

public class Client {
    public static void main(String[] args) {
        Car car = new GasolineCar();         // 获取汽车实例
        addRawPowerToCar(car);               // 汽车补充燃料
    }

    private static void addRawPowerToCar(Car car) {
     	System.out.println("1. 前去中石化加油站...");
     	System.out.println("2. 拿起95油枪...");
        car.refuel();
    }
}
public class GasolineCar extends Car {
    @Override
    public void refuel() {
        System.out.println("补充汽油燃料...");
    }
}

如上所示系统运行一切正常。此时如果业务需要添加一款新能源电车,此时你可能会考虑到继承Car,即可享受继承带来的复用性减少重复开发的人力,但似乎一切还为时尚早,我们先看下类图的改动:
在这里插入图片描述
类图中ElectricCar继承Car接口的refuel方法,内部实现应为:

public class ElectricCar extends Car{
    @Override
    public void refuel() {
        System.out.println("快速充电...");
    }
}

这里就存在问题,Car的refuel方法业务语义是"补充燃料(可燃烧)“,然而ElectricCar实现了refuel方法的语义改变为了"补充电力”,这就是前面说的子类实现的方法业务含义与基类方法不一致或不兼容。这导致问题是Client中的addRawPowerToCar方法将会出现歧义或异常,ElectricCar的实例也可以作为该方法的入参,但是行为确实不符合业务预期的,即电车怎么能够去中石化加油站拿起油枪进行快速充电呢,这就是可能存在的问题。因此ElectricCar继承Car就是不符合里氏替换原则。
那我们针对这个需求如何改动呢?首先我们知道新能源电车(ElectricCar)是不需要加油的,不需要也不能重写refuel方法。因此我们可以基类Car中补充能源的方法。类图及核心代码如下:
在这里插入图片描述

public class GasolineCar extends Car {
    @Override
    public void refuel() {
        System.out.println("补充汽油燃料...");
    }
    @Override
    public void charge() {
        throw new UnsupportedOperationException("GasolineCar 不支持充电");
    }
}
public class ElectricCar extends Car{
    @Override
    public void refuel() {
        throw new UnsupportedOperationException("ElectricCar 不支持添加燃油");
    }
    @Override
    public void charge() {
        System.out.println("加油充电...");
    }
}

如上所示,Car增加charge方法表示补充电车,ElectricCar 继承Car时必须重写refuel方法,通过内部抛出异常的方式告知ElectricCar 不允许调用该方法,上游业务侧代码应该做好充分的考虑。相对于上面不符合里氏替换原则的代码,还有一个好处。如果后续增加混动汽车(HybridCar)也可以继承Car方法并分别重写refuel方法和charge,将补充燃料和补充电能业务功能区分开。因此这也表明了符合里氏替换原则的代码也有利于代码符合开闭原则的要求
我们再继续思考下,第二版的方案虽然能够满足开闭原则及里氏替换原则,那符合单一职责原则吗?我认为是不符合的,从代码运行时的角度上将,确实ElectricCar#refuel()、GasolineCar#charge()会抛出异常,但是如果之后公司业务下线燃油车,ElectricCar是否也需要跟着变动,这就不符合单一职责原则了。不仅如此,如果业务之后再添加其他能源的车,如太阳能、煤炭能、氢能等等(ps,我只是举个栗子),那基类Car及其子类是否都需要改动,并且某种能源类型会有多少方法会抛出异常,非常影响代码的整洁性。因此更优的方案考虑类图如下:(代码省略…)
在这里插入图片描述

三、补充说明

在学习里氏替换原则时,翻了下网上关于该原则的讲述,对于其中的举例和理解我个人表示不是十分赞同。这一类教程会通过"正常形非长方形"、“鸵鸟非鸟”、"几维鸟非鸟"来说明里氏替换原则的意义。个人不认为这些是里氏替换原则要解决的问题,原则要解决的是子类实现的基类方法要和基类保持一致,即行为特征一致而非结构特征。换句话说,里氏替换原则是通过方法具备的业务含义(行为特征)来进一步完善继承机制,在之前继承是被片面的认为属性继承。
“正常形非长方形”,教程(本文不引用)中认为正方形具备"宽==高"的特点,而在长方形的业务代码中没有要求这个约束。假设此时业务代码中某方法,需要判断长方形面积是否为20,否则抛出异常。这样的方法对于正方形而言,是肯定报错的。但是这就是违背了里氏替换原则吗?试想长方形就一定不报错吗?正常形替换长方形传入判断面积,抛异常难道不符合业务逻辑?这个案例完全没有理解里氏替换原则的内涵,甚至行为方法都根本不涉及。
另外一点,这个案例还犯有另外一个错,也是学习设计模式的人经常混淆的问题。学习设计模式示例代码一定要区分业务代码以及设计模式代码。业务代码在本系列文章中使用Client类代替,在实际开发项目中业务代码是各种各样的。而设计模式代码就是我们通过精心设计的尽可能满足设计原则的代码。设计模式代码需要尽可能满足一切业务代码的要求,包括可扩展性、维护性、复用性等等。“长方形判断面积”这块代码就是属于业务代码,正方形继承于长方形,并不影响业务代码吧。
“鸵鸟非鸟”,教程中认为鸵鸟不会飞,因此在使用到bird.fly的业务代码中,不能让鸵鸟类继承于鸟类。首先里氏替换原则不是在讲类之前是否应该有继承关系,而且如何更好的使用继承关系且不使得业务代码困惑。鸵鸟类不能飞如本文第二章节所述电车不能加油一样,"鸵鸟不能飞"没有违背里氏替换原则,而电车在refuel方法中充电才是违背原则(业务代码会困惑、危险)。从继承机制上来说,继承的本质是为了代码复用以及扩展。子类是肯定允许有个性化内容的,谁规定鸟类就必须会飞?谁规定车Car就必须得加油?那你告诉我代码怎么改?本文最后的设计方案,不仅满足里氏替换原则,并且特斯拉电车也是继承或实现了Car基本类。【重点:里氏替换原则重点不是在讨论类之间应不应该继承的问题
"几维鸟非鸟"案例中,教程认为几维鸟行动缓慢,行动速度为0。这将导致业务代码中计算鸟类跑动一段路程所需的时间时,因为速度在分母上,几维鸟实例替换基类就会导致除0异常,不能替换。首先这个案例也是否定了子类应有的个性化内容,谁规定了鸟的速度不能为0?替换抛除0异常是业务代码考虑不全的问题,这问题跟几维鸟继承鸟根本没有关系。因为业务代码的问题或设计影响其依赖的设计模式代码完全是本末倒置。
【总结】

  • 里氏替换主要描述的是子类继承自基类的方法含义要和基类定义的业务含义一致
  • 类之间是否需要继承关系,主要考虑是否需要通过继承来获得代码复用和扩展的能力。这于里氏替换原则没有关系,先有继承关系,再考虑里氏替换原则。
  • 学习设计模式,区分业务代码和设计模式代码,并且前者依赖后者实现业务功能,后者需满足前者的变化并且具备扩展性、复用性、可读性等等。业务代码的设计不会影响到设计模式代码,而设计模式代码的设计会影响到业务代码的使用,切莫本末倒置。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/608388.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

智能计算补充(从第四章p44往后)

智能计算补充(从第四章p44往后) 本文内容大部分来自于任振兴老师的讲课PPT,主要是对老师PPT内容的总结和提炼,侵权请联系我删除。 文章目录 智能计算补充(从第四章p44往后)适应度尺度变换1、适应度尺度变换…

DicomObjects.Core 3.0.17 Crack

DicomObjects.NET 核心版简介 DicomObjects.Core Assembly DicomObjects.NET 核心版简介 DicomObjects.Core 由一组相互关联但独立的 .核心兼容的“对象”,使开发人员能够快速轻松地将DICOM功能添加到其产品中,而无需了解或编程DICOM标准的复杂性。此帮助…

Golang 协程/线程/进程 区别以及 GMP 详解

Golang 协程/线程/进程 区别详解 转载请注明来源:https://janrs.com/mffp 概念 进程 每个进程都有自己的独立内存空间,拥有自己独立的地址空间、独立的堆和栈,既不共享堆,亦不共享栈。一个程序至少有一个进程,一个进程…

数据库多表设计

说明:在项目的数据库设计时,表与表之间是有联系的,如学生管理系统中,有部门表,教师表、学生表、课程表等等 一位教师隶属于一个部门,一个部门有多位教师,因此部门表和教师表,是一对…

【c++修行之路】c++11特性--上

文章目录 前言列表初始化用法介绍原理:std::initializer_list 简化声明的方式autodecltype 右值引用移动构造万能引用和完美转发万能引用完美转发 类的新增功能可变参数模板lambda表达式深入探究lambda表达式lambda表达式带来的便利结语 前言 大家好久不见&#xf…

bmp文件格式与保存

BMP文件由三部分组成,分辨是文件头,DIM头和像素数据。具体格式如下: 基本介绍 1. 文件头 14个字节 signature: 为文件标志位,恒为0X42 FileSize:是指整个文件的大小 REservedx:保留位恒为0 …

leetcode700. 二叉搜索树中的搜索(java)

二叉搜索树中的搜索 leetcode700 二叉搜索树中的搜索题目描述 解题思路代码演示二叉树专题 leetcode700 二叉搜索树中的搜索 leetcode 700 二叉搜索树中的搜索。 来源:力扣(LeetCode) 链接:https://leetcode.cn/problems/search-i…

chatgpt赋能python:Python去掉GIL:提升Python多线程编程性能的突破口

Python去掉GIL:提升Python多线程编程性能的突破口 Python 是世界上最受欢迎的编程语言之一,其中一大原因是其简单易用、优雅简洁的语法。Python 也是一个卓越的多用途编程语言,广泛应用于 Web 开发、科学计算、人工智能等领域。但是&#xf…

javaScript蓝桥杯-----宝贵的一票

目录 一、介绍二、准备三、目标四、代码五、检测踩坑!!六、完成 一、介绍 公司经常举办各种活动,但一到投票环节就犯了难,于是公司决定安排小蓝开发一个投票系统,更好的收集大家的投票信息。为了赶在下一次活动开始前…

k8s亲和性和反亲和性

1.前言 k8s的亲和性和反亲和性都是通过标签来影响pod的调度,在此基础上亲和性又分为硬亲和性和软亲和性,required为硬亲和性即标签内容必须要符合才能调度,preferred为软亲和性即标签内容不一定要符合也能调度,除此之外还有node亲…

Unsupported major.minor version 51.0解决办法

先看看我的报错截图 [INFO] ------------------------------------------------------------------------ [INFO] BUILD FAILURE [INFO] ------------------------------------------------------------------------ [INFO] Total time: 08:51 min [INFO] Finished at: 2023-0…

[2.0快速体验]Apache Doris 2.0 日志分析快速体验

1. 概述 应用程序、服务器、云基础设施、IoT 和移动设备、DevOps、微服务架构—最重要的业务和 IT 发展趋势帮助我们以前所未有的方式优化运维和客户体验。但这些趋势也导致由机器生成的数据出现爆炸式成长,其中包括日志和指标等,例如,用户交…

Qcon 广州主题演讲:融云实时社区的海量消息分发实践

移步公众号文章 预约纸质版《作战地图》 5 月 26 日-27 日,QCon 全球软件开发大会落地广州。关注【融云全球互联网通信云】了解更多 融云 IM 服务架构师罗伟受邀分享“实时社区的海量消息分发实践”,从实践中来的前沿技术分享,收获现场开发者…

Nautilus Chain上首个DEX PoseiSwap即将开启IDO,潜力几何?

据悉,Nautilus Chain 上的首个 DEX PoseiSwap 即将开启 IDO ,根据官方的最新公告显示,PoseiSwap 即将于 6 月 13 日至 6 月 14 日期间,在 Bounce 平台开启其治理通证 $POSE 的 IDO(Initial DEX Offering)&a…

openEuler Linux 部署 FineReport

openEuler Linux 部署 FineReport 部署环境 环境版本openEuler Linux22.03MySQL8.0.33FineReport11.0 环境准备 升级系统内核和软件 yum -y updatereboot安装常用工具软件 yum -y install vim tar net-tools 安装MySQL8 将 MySQL Yum 存储库添加到系统的存储库列表中 s…

【JavaEE】表白墙再升级(MySQL实现持久化)

表白墙再升级(MySQL实现持久化) 文章目录 【JavaEE】表白墙再升级(MySQL实现持久化)1. 后端引入JDBC的依赖2. 建库建表3. 编写数据库代码(JDBC)3.1 doGet方法改写3.1.1 构建本地数据源3.1.2 用本地数据源构…

移动端的轮播图

效果 技术选取 前端框架用的是vue3,使用的组件库为element-plus以及vant4 引入element-plus和vant4 安装element-plus cnpm install element-plus --save 安装按需导入 cnpm install -D unplugin-vue-components unplugin-auto-import 安装Vant cnpm i vant 按…

Fiddler抓包工具之fiddler的composer可以简单发送http协议的请求

一,composer的详解 右侧Composer区域,是测试接口的界面: 相关说明: 1.请求方式:点开可以勾选请求协议是get、post等 2.url地址栏:输入请求的url地址 3.请求头:第三块区域可以输入请求头信息…

图的简单理解

文章目录 1、图的基本概念2、图的存储结构2.1 邻接矩阵2.2 邻接表 3、图的遍历3.1 广度优先遍历3.2 深度优先遍历 4、最小生成树4.1 Kruskal算法4.2 Prim算法 5、最短路径5.1 单源最短路径–Dijkstra算法5.2 单源最短路径–Bellman-Ford算法5.3 多源最短路径 1、图的基本概念 …

路径规划算法:基于差分进化优化的路径规划算法- 附代码

路径规划算法:基于差分进化优化的路径规划算法- 附代码 文章目录 路径规划算法:基于差分进化优化的路径规划算法- 附代码1.算法原理1.1 环境设定1.2 约束条件1.3 适应度函数 2.算法结果3.MATLAB代码4.参考文献 摘要:本文主要介绍利用智能优化…