当前位置: 首页 > 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 数据结构可以节约存储空间,本文中结合企业真…...

关于iview组件中使用 table , 绑定序号分页后序号从1开始的解决方案

问题描述:iview使用table 中type: "index",分页之后 ,索引还是从1开始,试过绑定后台返回数据的id, 这种方法可行,就是后台返回数据的每个页面id都不完全是按照从1开始的升序,因此百度了下,找到了…...

2024年赣州旅游投资集团社会招聘笔试真

2024年赣州旅游投资集团社会招聘笔试真 题 ( 满 分 1 0 0 分 时 间 1 2 0 分 钟 ) 一、单选题(每题只有一个正确答案,答错、不答或多答均不得分) 1.纪要的特点不包括()。 A.概括重点 B.指导传达 C. 客观纪实 D.有言必录 【答案】: D 2.1864年,()预言了电磁波的存在,并指出…...

linux arm系统烧录

1、打开瑞芯微程序 2、按住linux arm 的 recover按键 插入电源 3、当瑞芯微检测到有设备 4、松开recover按键 5、选择升级固件 6、点击固件选择本地刷机的linux arm 镜像 7、点击升级 (忘了有没有这步了 估计有) 刷机程序 和 镜像 就不提供了。要刷的时…...

Spring数据访问模块设计

前面我们已经完成了IoC和web模块的设计,聪明的码友立马就知道了,该到数据访问模块了,要不就这俩玩个6啊,查库势在必行,至此,它来了。 一、核心设计理念 1、痛点在哪 应用离不开数据(数据库、No…...

OPENCV形态学基础之二腐蚀

一.腐蚀的原理 (图1) 数学表达式:dst(x,y) erode(src(x,y)) min(x,y)src(xx,yy) 腐蚀也是图像形态学的基本功能之一,腐蚀跟膨胀属于反向操作,膨胀是把图像图像变大,而腐蚀就是把图像变小。腐蚀后的图像变小变暗淡。 腐蚀…...

VM虚拟机网络配置(ubuntu24桥接模式):配置静态IP

编辑-虚拟网络编辑器-更改设置 选择桥接模式,然后找到相应的网卡(可以查看自己本机的网络连接) windows连接的网络点击查看属性 编辑虚拟机设置更改网络配置,选择刚才配置的桥接模式 静态ip设置: 我用的ubuntu24桌…...

算法:模拟

1.替换所有的问号 1576. 替换所有的问号 - 力扣(LeetCode) ​遍历字符串​:通过外层循环逐一检查每个字符。​遇到 ? 时处理​: 内层循环遍历小写字母(a 到 z)。对每个字母检查是否满足: ​与…...

【电力电子】基于STM32F103C8T6单片机双极性SPWM逆变(硬件篇)

本项目是基于 STM32F103C8T6 微控制器的 SPWM(正弦脉宽调制)电源模块,能够生成可调频率和幅值的正弦波交流电源输出。该项目适用于逆变器、UPS电源、变频器等应用场景。 供电电源 输入电压采集 上图为本设计的电源电路,图中 D1 为二极管, 其目的是防止正负极电源反接, …...

Golang——7、包与接口详解

包与接口详解 1、Golang包详解1.1、Golang中包的定义和介绍1.2、Golang包管理工具go mod1.3、Golang中自定义包1.4、Golang中使用第三包1.5、init函数 2、接口详解2.1、接口的定义2.2、空接口2.3、类型断言2.4、结构体值接收者和指针接收者实现接口的区别2.5、一个结构体实现多…...

十九、【用户管理与权限 - 篇一】后端基础:用户列表与角色模型的初步构建

【用户管理与权限 - 篇一】后端基础:用户列表与角色模型的初步构建 前言准备工作第一部分:回顾 Django 内置的 `User` 模型第二部分:设计并创建 `Role` 和 `UserProfile` 模型第三部分:创建 Serializers第四部分:创建 ViewSets第五部分:注册 API 路由第六部分:后端初步测…...