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

面向对象设计之里氏替换原则

 设计模式专栏:http://t.csdnimg.cn/4Mt4u 

 思考:什么样的代码才算违反里氏替换原则?

目录

1.里氏替换原则的定义

2.里氏替换原则与多态的区别

3.违反里氏替换原则的反模式

4.总结


1.里氏替换原则的定义

        里氏替换原则(Liskov Substitution principle)是由芭芭拉·利斯科夫(Barbara Liskov)在1987年在一次会议上名为“数据的抽象与层次”的演说中首先提出。他当时是这样描述这条原则的:如果S是T的子类型,那么T的对象可以被S的对象所替换,并不影响代码的运行。1966年,Robert Martin在他的SOLID原则中重新描述了里氏替换原则:使用父类对象的函数可以在不了解子类的情况下替换为使用子类对象。

        结合Bartbara Liskov和Robert Martin 的描述,我们将里氏替换原则描述为:子类对象(object of subtype/derived class)能够替换到程序(program)中父类对象(object of base/parent class)出现的任何地方,并且保证程序原有的逻辑行为(behavior)不变和正确性不被破坏。

        里氏替换原则的定义比较抽象,我们通过一个代码示例进行解释。其中,父类 Transporter 使用org.apache.http库中的 HttpChient 类传输网络数据;子类 SecurityTransporter 继承父类 Transporter增加了一些额外的功能,支持在传输数据的同时传输 appld和 appToken 安全认证信息。

public class Transporter {private Httpclient httpclient;public Transporter(Httpclient httpclient){this.httpclient = httpclient;}public Response sendRequest(Request request){//...省略使用httpclient发送请求的代码逻辑...}
}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 demorunction(Transporter transporter){Reugest request = new Request();//...省略设置request中数据值的代码.Response         response = transporter.sendRequest (request);//...省略其他逻辑...}
}//里氏替换原则
Demo demo = new Demo();
demo.demofunction (new securityTransporter(/*省略參数*/);)

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

2.里氏替换原则与多态的区别

        不过,读者可能会有疑问:上述代码设计不就是简单利用了面向对象的多态特性吗?多态和里氏替换原则是不是一回事?从上面的代码示例和里氏替换原则的定义来看,里氏替类与多态看起来类似,但实际上它们完全是两回事。

        我们还是通过上面的代码示例进行解释。不过,我们需要对SecuityTransporer类中sendRequest0函数稍加改造。改造前,如果appld或 appToken 没有设置,则不做安全校验改造后,如果 appId或 appToken 没有设置,则直接抛出 NoAuthorizationRunfimeException未授权异常。改造前后的代码对比如下。

//改造前:
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(approken)){throw new NoAuthorizationRuntimeException(...);}request.addPayload ("app-id", appId) ;request .addPayload ("app-token", appToken);return super.sendRequest(request);}
}

        在改造后的代码中,如果传入demoFunction()函数的是父类Transporter 的对象,那么demoFuncion()函数并不会抛出异常,但如果传入demoFuncion()函数的是子类 SecurityTransporter的对象,那么 demoFuncion()有可能抛出异常。尽管代码中抛出的是运行时异常(Runtime Exception),可以不在代码中显式地捕获处理,但子类替换父类并传入 demoFunction() 函数之后,整个程序的逻辑行为有了改变。
        虽然改造之后的代码仍然可以通过Java的多态语法动态地使用子类 SecwriyTansport替换父类Tansporer,也并不会导致程序编译或运行报错,但是,从设计思路上来讲SecurityTransporter的设计是不符合里氏替换原则的。多态是一种代码实现思路、而里氏替换原则是一种设计原则,用来指导维承关系中子类的设计:在换父类时、确保不改变程的逻辑行为,以及不破坏程序的正确性。

3.违反里氏替换原则的反模式

        实际上,里氏替换原则还有一个能落地且更有指导意义的描述,那就是按照协议来设计。在设计子类时,需要遵守父类的行为约定(或称为协议)。父类定义了函数的行为约定,子类可以改变函数内部实现逻辑,但本能改变函数原有的行为约定。这里的行为约定包括函数声明要实现的功能,对输入、输出和异常的约定,以及注释中罗列的任何特殊情况说明等。实际上、这里所讲的父类和子类的关系可以替换成接口和实现类的关系。
        为了更好地理解上述内容,我们提供若干违反里氏替换原则的例子。
1.子类违反父类声明要实现的功能
        例如,父类定义了一个订单排序函数sortOrdersByAmount(),该函数按照金额从小到大来给订单排序,而子类重写sorOrdersByAmount()之后,按照创建日期来给订单排序。那么,这个子类的设计就违反了里氏替换原则。
2.子类违反父类对输入、输出和异常的约定
        在父类中,某个函数约定:运行出错时返回null,获取数据为空时返回空集合(empty collection)。而子类重载此函数之后,重新定义了返回值:运行出错时返回异常(exception),
获取不到数据时返回 null。那么,这个子类的设计就违反了里氏替换原则。
        在父类中,某个函数约定:输入数据可以是任意整数,但子类重载此函数之后,只允许输入数据是正整数,如果是负数,就抛出异常,也就是说,子类对输入数据的校验比父类更加产格。那么,这个子类的设计就违反了里氏替换原则。
        在父类中,某个函数约定只抛出 ArgumentNullException 异常,那么子类重载此函数之后也只允许抛出 ArgumentNullException异常,否则子类就违反了里氏替换原则。
3.子类违反父类注释中罗列的任何特殊说明
        在父类中,定义了一个提现函数 withdraw(),其注释是这样写的:“用户的提现金额不得超过账户余额……”,而子类重写 withdraw() 函数之后,针对 VIP 账号实现了透支提现的功能,也就是提现金额可以大于账户余额。那么,这个子类的设计就不符合里氏替换原则。如果想要这个子类的设计符合里氏替换原则,那么,较为简单的办法是修改父类的注释。
        以上便是3种典型的违反里氏替换原则的反模式。
        除此之外,判断子类的设计实现是否违反里氏替换原则,还有一个小窍门,那就是用父类的单元测试验证子类的代码。如果某些单元测试运行失败,就说明子类的设计实现没有完全遵守父类的约定,子类有可能违反了里氏替换原则。

4.总结

        里氏替换原则是面向对象设计的基本原则之一。它强调在软件设计中,子类对象应当能够替换其父类对象,并且替换后,程序的行为应当保持不变。这一原则确保了软件系统的稳定性和可扩展性。

    里氏替换原则的核心思想可以概括为:

  1. 子类应当能够替换其父类,并且在替换后,程序的行为应当保持不变。这意味着子类必须完全遵守父类的行为约定,即子类不能改变父类原有的功能。
  2. 子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说,子类在继承父类的基础上,可以添加新的方法或属性,但不能覆盖或修改父类的非抽象方法。

        里氏替换原则的实现有助于保持软件系统的稳定性和灵活性。通过使用基类类型来对对象进行定义,可以在运行时根据实际需要替换为不同的子类对象,从而实现多态性。这种设计方式使得软件系统更加易于维护和扩展,同时也提高了代码的可重用性。

        然而,在实际应用中,要完全遵守里氏替换原则并不容易。有时,为了实现特定的功能或优化性能,可能会需要对父类的方法进行覆盖或修改。在这种情况下,需要仔细权衡利弊,确保修改后的子类仍然能够保持与父类相似的行为,并且不会对现有的代码产生不良影响。

        总之,里氏替换原则是面向对象设计中的重要原则之一,它有助于确保软件系统的稳定性和可扩展性。在设计和开发过程中,应当尽量遵守这一原则,以实现高质量的软件系统。

相关文章:

面向对象设计之里氏替换原则

设计模式专栏:http://t.csdnimg.cn/4Mt4u 思考:什么样的代码才算违反里氏替换原则? 目录 1.里氏替换原则的定义 2.里氏替换原则与多态的区别 3.违反里氏替换原则的反模式 4.总结 1.里氏替换原则的定义 里氏替换原则(Liskov S…...

MySQL·SQL优化

目录 一 . 前言 二 . 优化方法 1 . 索引 (1)数据构造 (2)单索引 (3)explain (4)组合索引 (5)索引总结 2 . 避免使用select * 3 . 用union all代替u…...

Dockerfile指令大全

Dockerfile文件由一系列指令和参数组成。指令的一般格式为INSTRUCTION arguments。具体来说,包括"配置指令"(配置镜像信息)和"操作指令"(具体执行操作)。每条指令,如FROM,都是大小写不敏感的。但是为了区分指令和参数&am…...

第八个实验:(A+B)-C的结果判断奇偶特性

实验内容:(A+B)-C的结果判断奇偶特性,最后显示结果 实验步骤: 第一步:建立项目 第二步:实验步骤,编写程序 第三步:实验结果...

设计模式:观察者模式 ⑧

一、思想 观察者模式是一种常见的设计模式,也称作发布-订阅模式。它主要解决了对象之间的通知依赖关系问题。在这种模式中,一个对象(称作Subject)维护着一个对象列表,这些对象(称作Observers)都…...

【重温设计模式】迭代器模式及其Java示例

迭代器模式的介绍 在编程领域,迭代器模式是一种常见的设计模式,它提供了一种方法,使得我们可以顺序访问一个集合对象中的各个元素,而又无需暴露该对象的内部表示。你可以把它想象成一本书,你不需要知道这本书是怎么印…...

(001)UV 的使用以及导出

文章目录 UV窗口导出模型的主要事项导出时材质的兼容问题unity贴图导出导出FBX附录 UV窗口 1.uv主要的工作区域: 2.在做 uv 和贴图之前,最好先应用下物体的缩放、旋转。 导出模型的主要事项 1.将原点设置到物体模型的底部: 2.应用修改器的…...

一文理解CAS和自旋的区别(荣耀典藏版)

目录 一、自旋 二、CAS 三、什么是 ABA 问题 大家好,我是月夜枫,通常在面试的时候,或者在学习的时候,经常性的会遇到一些关于锁的问题,尤其是面试官会提出提问,你对锁了解的多么?你知道锁的原…...

【吊打面试官系列】Java虚拟机JVM篇 - 关于内存溢出

大家好,我是锋哥。今天分享关于内存溢出的JVM面试题,希望对大家有帮助; 什么是内存溢出? 内存溢出(OOM)是指可用内存不足。程序运行需要使用的内存超出最大可用值,如果不进行处理就会影响到其他…...

思科网络中如何配置标准ACL协议

一、什么是标准ACL协议?有什么作用及配置方法? (1)标准ACL(Access Control List)协议是一种用于控制网络设备上数据流进出的协议。标准ACL基于源IP地址来过滤数据流,可以允许或拒绝特定IP地址范…...

蓝桥杯刷题(二)

参考大佬代码:(区间合并二分) import os import sysn, L map(int, input().split()) # 输入n,len arr [list(map(int, input().split())) for _ in range(n)] # 输入Li,Si def check(Ti, arr, L)->bool:sec [] # 存入已打开的阀门在…...

【Python】牛客网—软件开发-Python专项练习(day1)

1.(单选)下面哪个是Python中不可变的数据结构? A.set B.list C.tuple D.dict 可变数据类型:列表list[ ]、字典dict{ }、集合set{ }(能查询,也可更改)数据发生改变,但内存地址不变 不…...

P3405 [USACO16DEC] Cities and States S题解

题目 Farmer John有若干头奶牛。为了训练奶牛们的智力,Farmer John在谷仓的墙上放了一张美国地图。地图上表明了每个城市及其所在州的代码(前两位大写字母)。 由于奶牛在谷仓里花了很多时间看这张地图,他们开始注意到一些奇怪的…...

JavaScript原型和原型链

JavaScript每个对象拥有一个原型对象 需要注意的是,只有函数对象才有 prototype 属性 当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索&#xff…...

PyTorch之完整的神经网络模型训练

简单的示例: 在PyTorch中,可以使用nn.Module类来定义神经网络模型。以下是一个示例的神经网络模型定义的代码: import torch import torch.nn as nnclass MyModel(nn.Module):def __init__(self):super(MyModel, self).__init__()# 定义神经…...

基于神经网络的偏微分方程求解器再度取得突破,北大字节的研究成果入选Nature子刊

目录 一.引言:神经网络与偏微分方程 二.如何基于神经网络求解偏微分方程 1.简要概述 2.基于神经网络求解偏微分方程的三大方向 2.1数据驱动 基于CNN 基于其他网络 2.2物理约束 PINN 基于 PINN 可测量标签数据 2.3物理驱动(纯物理约束) 全连接神经网路(FC-NN) CN…...

Linux的基本权限

一、对shell的浅显认识 shell是操作系统下的一个外壳程序,无论是Linux操作系统,还是Windows操作系统,用户都不会直接对操作系统本身直接进行操作,需要通过一个外壳程序去间接的进行各种操作 在Linux的shell外壳就是命令行&#…...

指纹加密U盘/指纹KEY方案——采用金融级安全芯片 ACH512

方案概述 指纹加密U盘解决方案可实现指纹算法处理、数据安全加密、数据高速存取(EMMC/TF卡/NandFlash),可有效保护用户数据安全。 方案特点 • 采用金融级安全芯片 ACH512 • 存储介质:EMMC、TF卡、NandFlash • 支持全系列国密…...

Cloud-Sleuth分布式链路追踪(服务跟踪)

简介 在微服务框架中,一个由客户端发起的请求在后端系统中会经过多个不同的服务节点调用来协同产生最后的请求结果,每一个前端请求都会形成一条复杂的分布式服务调用链路,链路中的任何一环出现高延时或错误都会引起整个请求最后的失败 GitHub - spring-cloud/spring-cloud-sl…...

flink重温笔记(十四): flink 高级特性和新特性(3)——数据类型及 Avro 序列化

Flink学习笔记 前言:今天是学习 flink 的第 14 天啦!学习了 flink 高级特性和新特性之数据类型及 avro 序列化,主要是解决大数据领域数据规范化写入和规范化读取的问题,avro 数据结构可以节约存储空间,本文中结合企业真…...

[2025CVPR]DeepVideo-R1:基于难度感知回归GRPO的视频强化微调框架详解

突破视频大语言模型推理瓶颈,在多个视频基准上实现SOTA性能 一、核心问题与创新亮点 1.1 GRPO在视频任务中的两大挑战 ​安全措施依赖问题​ GRPO使用min和clip函数限制策略更新幅度,导致: 梯度抑制:当新旧策略差异过大时梯度消失收敛困难:策略无法充分优化# 传统GRPO的梯…...

基于大模型的 UI 自动化系统

基于大模型的 UI 自动化系统 下面是一个完整的 Python 系统,利用大模型实现智能 UI 自动化,结合计算机视觉和自然语言处理技术,实现"看屏操作"的能力。 系统架构设计 #mermaid-svg-2gn2GRvh5WCP2ktF {font-family:"trebuchet ms",verdana,arial,sans-…...

ESP32 I2S音频总线学习笔记(四): INMP441采集音频并实时播放

简介 前面两期文章我们介绍了I2S的读取和写入,一个是通过INMP441麦克风模块采集音频,一个是通过PCM5102A模块播放音频,那如果我们将两者结合起来,将麦克风采集到的音频通过PCM5102A播放,是不是就可以做一个扩音器了呢…...

学习STC51单片机31(芯片为STC89C52RCRC)OLED显示屏1

每日一言 生活的美好,总是藏在那些你咬牙坚持的日子里。 硬件:OLED 以后要用到OLED的时候找到这个文件 OLED的设备地址 SSD1306"SSD" 是品牌缩写,"1306" 是产品编号。 驱动 OLED 屏幕的 IIC 总线数据传输格式 示意图 …...

【论文阅读28】-CNN-BiLSTM-Attention-(2024)

本文把滑坡位移序列拆开、筛优质因子,再用 CNN-BiLSTM-Attention 来动态预测每个子序列,最后重构出总位移,预测效果超越传统模型。 文章目录 1 引言2 方法2.1 位移时间序列加性模型2.2 变分模态分解 (VMD) 具体步骤2.3.1 样本熵(S…...

RNN避坑指南:从数学推导到LSTM/GRU工业级部署实战流程

本文较长,建议点赞收藏,以免遗失。更多AI大模型应用开发学习视频及资料,尽在聚客AI学院。 本文全面剖析RNN核心原理,深入讲解梯度消失/爆炸问题,并通过LSTM/GRU结构实现解决方案,提供时间序列预测和文本生成…...

Java毕业设计:WML信息查询与后端信息发布系统开发

JAVAWML信息查询与后端信息发布系统实现 一、系统概述 本系统基于Java和WML(无线标记语言)技术开发,实现了移动设备上的信息查询与后端信息发布功能。系统采用B/S架构,服务器端使用Java Servlet处理请求,数据库采用MySQL存储信息&#xff0…...

【LeetCode】3309. 连接二进制表示可形成的最大数值(递归|回溯|位运算)

LeetCode 3309. 连接二进制表示可形成的最大数值(中等) 题目描述解题思路Java代码 题目描述 题目链接:LeetCode 3309. 连接二进制表示可形成的最大数值(中等) 给你一个长度为 3 的整数数组 nums。 现以某种顺序 连接…...

脑机新手指南(七):OpenBCI_GUI:从环境搭建到数据可视化(上)

一、OpenBCI_GUI 项目概述 (一)项目背景与目标 OpenBCI 是一个开源的脑电信号采集硬件平台,其配套的 OpenBCI_GUI 则是专为该硬件设计的图形化界面工具。对于研究人员、开发者和学生而言,首次接触 OpenBCI 设备时,往…...

日常一水C

多态 言简意赅:就是一个对象面对同一事件时做出的不同反应 而之前的继承中说过,当子类和父类的函数名相同时,会隐藏父类的同名函数转而调用子类的同名函数,如果要调用父类的同名函数,那么就需要对父类进行引用&#…...