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

2024-03-10 23:04

本文主要是介绍面向对象设计之里氏替换原则,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

 设计模式专栏: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://www.chinasem.cn/article/795809

相关文章

不懂推荐算法也能设计推荐系统

本文以商业化应用推荐为例,告诉我们不懂推荐算法的产品,也能从产品侧出发, 设计出一款不错的推荐系统。 相信很多新手产品,看到算法二字,多是懵圈的。 什么排序算法、最短路径等都是相对传统的算法(注:传统是指科班出身的产品都会接触过)。但对于推荐算法,多数产品对着网上搜到的资源,都会无从下手。特别当某些推荐算法 和 “AI”扯上关系后,更是加大了理解的难度。 但,不了解推荐算法,就无法做推荐系

怎么让1台电脑共享给7人同时流畅设计

在当今的创意设计与数字内容生产领域,图形工作站以其强大的计算能力、专业的图形处理能力和稳定的系统性能,成为了众多设计师、动画师、视频编辑师等创意工作者的必备工具。 设计团队面临资源有限,比如只有一台高性能电脑时,如何高效地让七人同时流畅地进行设计工作,便成为了一个亟待解决的问题。 一、硬件升级与配置 1.高性能处理器(CPU):选择多核、高线程的处理器,例如Intel的至强系列或AMD的Ry

基于51单片机的自动转向修复系统的设计与实现

文章目录 前言资料获取设计介绍功能介绍设计清单具体实现截图参考文献设计获取 前言 💗博主介绍:✌全网粉丝10W+,CSDN特邀作者、博客专家、CSDN新星计划导师,一名热衷于单片机技术探索与分享的博主、专注于 精通51/STM32/MSP430/AVR等单片机设计 主要对象是咱们电子相关专业的大学生,希望您们都共创辉煌!✌💗 👇🏻 精彩专栏 推荐订阅👇🏻 单片机

SprinBoot+Vue网络商城海鲜市场的设计与实现

目录 1 项目介绍2 项目截图3 核心代码3.1 Controller3.2 Service3.3 Dao3.4 application.yml3.5 SpringbootApplication3.5 Vue 4 数据库表设计5 文档参考6 计算机毕设选题推荐7 源码获取 1 项目介绍 博主个人介绍:CSDN认证博客专家,CSDN平台Java领域优质创作者,全网30w+

JVM内存调优原则及几种JVM内存调优方法

JVM内存调优原则及几种JVM内存调优方法 1、堆大小设置。 2、回收器选择。   1、在对JVM内存调优的时候不能只看操作系统级别Java进程所占用的内存,这个数值不能准确的反应堆内存的真实占用情况,因为GC过后这个值是不会变化的,因此内存调优的时候要更多地使用JDK提供的内存查看工具,比如JConsole和Java VisualVM。   2、对JVM内存的系统级的调优主要的目的是减少

单片机毕业设计基于单片机的智能门禁系统的设计与实现

文章目录 前言资料获取设计介绍功能介绍程序代码部分参考 设计清单具体实现截图参考文献设计获取 前言 💗博主介绍:✌全网粉丝10W+,CSDN特邀作者、博客专家、CSDN新星计划导师,一名热衷于单片机技术探索与分享的博主、专注于 精通51/STM32/MSP430/AVR等单片机设计 主要对象是咱们电子相关专业的大学生,希望您们都共创辉煌!✌💗 👇🏻 精彩专栏 推荐订

Spring的设计⽬标——《Spring技术内幕》

读《Spring技术内幕》第二版,计文柯著。 如果我们要简要地描述Spring的设计⽬标,可以这么说,Spring为开发者提供的是⼀个⼀站式的轻量级应⽤开发框架(平台)。 作为平台,Spring抽象了我们在 许多应⽤开发中遇到的共性问题;同时,作为⼀个轻量级的应⽤开发框架,Spring和传统的J2EE开发相⽐,有其⾃⾝的特点。 通过这些⾃⾝的特点,Spring充分体现了它的设计理念:在

开题报告中的研究方法设计:AI能帮你做什么?

AIPaperGPT,论文写作神器~ https://www.aipapergpt.com/ 大家都准备开题报告了吗?研究方法部分是不是已经让你头疼到抓狂? 别急,这可是大多数人都会遇到的难题!尤其是研究方法设计这一块,选定性还是定量,怎么搞才能符合老师的要求? 每次到这儿,头脑一片空白。 好消息是,现在AI工具火得一塌糊涂,比如ChatGPT,居然能帮你在研究方法这块儿上出点主意。是不

创业者该如何设计公司的股权架构

本文来自七八点联合IT橘子和车库咖啡的一系列关于设计公司股权结构的讲座。 主讲人何德文: 在公司发展的不同阶段,创业者都会面临公司股权架构设计问题: 1.合伙人合伙创业第一天,就会面临股权架构设计问题(合伙人股权设计); 2.公司早期要引入天使资金,会面临股权架构设计问题(天使融资); 3.公司有三五十号人,要激励中层管理与重要技术人员和公司长期走下去,会面临股权架构设计问题(员工股权激

ffmpeg面向对象-待定

1.常用对象 rtsp拉流第一步都是avformat_open_input,其入参可以看下怎么用: AVFormatContext *fmt_ctx = NULL;result = avformat_open_input(&fmt_ctx, input_filename, NULL, NULL); 其中fmt_ctx 如何分配内存的?如下 int avformat_open_input(