设计原则(二)里氏替换原则(LSP)
发布日期:2021-09-17 01:32:54 浏览次数:3 分类:技术文章

本文共 5040 字,大约阅读时间需要 16 分钟。

一、什么是里氏替换原则

里氏替换原则的严格表达是:

如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1都替换成o2时,程序P的行为没有变化,那么类型T2是类型T1的子类型。

换言之,一个软件实体如果使用的是一个基类的话,那么一定适用于其子类,而且它根本不能察觉出基类对象和子类对象的区别。

比如,假设有两个类,一个是Base类,另一个是Child类,并且Child类是Base的子类。那么一个方法如果可以接受一个基类对象b的话:method1(Base b)那么它必然可以接受一个子类的对象method1(Child c).

里氏替换原则是继承复用的基石。只有当衍生类可以替换掉基类,软件单位的功能不会受到影响时,基类才能真正的被复用,而衍生类也才能够在基类的基础上增加新的行为。

但是需要注意的是,反过来的代换是不能成立的,如果一个软件实体使用的是一个子类的话,那么它不一定适用于基类。如果一个方法method2接受子类对象为参数的话method2(Child c),那么一般而言不可以有method2(b).

二、墨子的智慧

《墨子:小取》中说,“白马,马也;乘白马,乘马也。骊马,马也;乘骊马,乘马也”。文中的骊马是黑的马。意思就是白马和黑马都是马,乘白马或者乘黑马就是乘马。在面向对象中我们可以这样理解,马是一个父类,白马和黑马都是马的子类,我们说乘马是没有问题的,那么我们把父类换成具体的子类,也就是乘白马和乘黑马也是没有问题的,这就是我们上边说的里氏替换原则。

墨子同时还指出了反过来是不能成立的。《墨子:小取》中说:“娣,美人也,爱娣,非爱美人也”。娣是指妹妹,也就是说我的妹妹是没人,我爱我的妹妹(出于兄妹感情),但是不等于我爱美人。在面向对象里就是,美人是一个父类,妹妹是美人的一个子类。哥哥作为一个类有“喜爱()”方法,可以接受妹妹作为参量。那么这个“喜爱()”不能接受美人类的实例,这也就说明了反过来是不能成立的。

三、正方形是不是长方形

上过数学课的人都知道,正方形是一种特殊的长方形,只不过是它的长和宽是一样的,也就是说我们在面向对象里我们应当将长方形设计成父类,将正方形设计成长方形的子类,但是我可以很负责的告诉你,这样做是错误的,是不符合里氏替换原则的。

package com.designphilsophy.lsp.version1;/** * 定义一个长方形类,只有标准的get和set方法 *  * @author xingjiarong * */public class Rectangle {
protected long width; protected long height; public void setWidth(long width) { this.width = width; } public long getWidth() { return this.width; } public void setHeight(long height) { this.height = height; } public long getHeight() { return this.height; }}
package com.designphilsophy.lsp.version1;/** * 定义一个正方形类继承自长方形类,只有一个side *  * @author xingjiarong * */public class Square extends Rectangle {
public void setWidth(long width) { this.height = width; this.width = width; } public long getWidth() { return width; } public void setHeight(long height) { this.height = height; this.width = height; } public long getHeight() { return height; }}
package com.designphilsophy.lsp.version1;public class SmartTest{
/** * 长方形的长不短的增加直到超过宽 * @param r */ public void resize(Rectangle r) { while (r.getHeight() <= r.getWidth() ) { r.setHeight(r.getHeight() + 1); } }}
在上边的代码中我们定义了一个长方形和一个继承自长方形的正方形,看着是非常符合逻辑的,但是当我们调用SmartTest类中的resize方法时,长方形是可以的,但是正方形就会一直增大,一直long溢出。但是我们按照我们的里氏替换原则,父类可以的地方,换成子类一定也可以,所以上边的这个例子是不符合里氏替换原则的。
问题由来:有一功能P1,由类A完成。现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障。
解决方案:当使用继承时,遵循里氏替换原则。类B继承类A时,除添加新的方法完成新增功能P2外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法。

刚才我们写的代码的结构就是上边那样的,对于这样不符合里氏替换原则原则的关系,我们在代码重构的时候一般采用下面的方法。

我们再定义一个他们共同的父类,然后让正方形和长方形都继承自这个父类。

具体的代码如下:

package com.designphilsophy.lsp.version2;/** * 定义一个四边形类,只有get方法没有set方法 * @author xingjiarong * */public abstract class Quadrangle {
protected abstract long getWidth(); protected abstract long getHeight();}
package com.designphilsophy.lsp.version2;/** * 自己声明height和width * @author xingjiarong * */public class Rectangle extends Quadrangle {
private long width; private long height; public void setWidth(long width) { this.width = width; } public long getWidth() { return this.width; } public void setHeight(long height) { this.height = height; } public long getHeight() { return this.height; }}
package com.designphilsophy.lsp.version2;/** * 自己声明height和width * @author xingjiarong * */public class Square extends Quadrangle {
private long width; private long height; public void setWidth(long width) { this.height = width; this.width = width; } public long getWidth() { return width; } public void setHeight(long height) { this.height = height; this.width = height; } public long getHeight() { return height; }}

在基类Quadrange类中没有赋值方法,因此类似于SamrtTest的resize()方法不可能适用于Quadrangle类型,而只能适用于不同的具体子类Rectangle和Aquare,因此里氏替换原则不可能被破坏了。

四、为什么要符合里氏替换原则

里氏替换原则通俗的来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。它包含以下4层含义:

  • 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
  • 子类中可以增加自己特有的方法。
  • 当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
  • 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。

看上去很不可思议,因为我们会发现在自己编程中常常会违反里氏替换原则,程序照样跑的好好的。所以大家都会产生这样的疑问,假如我非要不遵循里氏替换原则会有什么后果?来看一个例子。

package com.designphilsophy.lsp.version3;public class A{    public int func1(int a, int b){        return a-b;    }}
package com.designphilsophy.lsp.version3;public class B extends A{
public int func1(int a, int b){ return a+b; } public int func2(int a, int b){ return func1(a,b)+100; } }
package com.designphilsophy.lsp.version3;public class Client{      public static void main(String[] args){          B b = new B();          System.out.println("100-50="+b.func1(100, 50));          System.out.println("100-80="+b.func1(100, 80));          System.out.println("100+20+100="+b.func2(100, 20));      }  }

输入结果:

100-50=150

100-80=180
100+20+100=220

我们发现原本运行正常的相减功能发生了错误。原因就是类B在给方法起名时无意中重写了父类的方法,造成所有运行相减功能的代码全部调用了类B重写后的方法,造成原本运行正常的功能出现了错误。在本例中,引用基类A完成的功能,换成子类B之后,发生了异常。在实际编程中,我们常常会通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的几率非常大。

源码下载:

转载地址:https://blog.csdn.net/xingjiarong/article/details/50081857 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:设计原则(三)组合复用原则
下一篇:设计模式(七)门面模式(Facade Pattern 外观模式)

发表评论

最新留言

能坚持,总会有不一样的收获!
[***.219.124.196]2024年03月25日 23时55分17秒

关于作者

    喝酒易醉,品茶养心,人生如梦,品茶悟道,何以解忧?唯有杜康!
-- 愿君每日到此一游!

推荐文章

mysql记录虚拟货币数据类型_近几年虚拟货币应用情况的相关数据 2021-06-24
ntp同步 mysql_解析Mysql 主从同步延迟原理及解决方案 2021-06-24
python从大到小排序_python作业:用嵌套的列表存储学生成绩数据,并编程完成如下操作... 2021-06-24
stm32的rxne和idle中断_STM32 HAL CubeMX 串口IDLE接收空闲中断+DMA 2021-06-24
mysql 逆序排序_将一组乱序的字符进行排序进行升序和逆序输出 2021-06-24
python中的cv2模块能否保存图像的地理坐标信息_Python中plt.plot图像保存有白边,CV2.polyline,fillpoly的参数问题,图像保存颜色发生异常... 2021-06-24
python 类继承方法_python类的继承、多继承及其常用魔术方法 2021-06-24
mysql配置多个磁盘_MySQL多实例配置(两) 2021-06-24
java开启一个线程_【jdk源码分析】java多线程开启的三种方式 2021-06-24
java数组重复_JAVA数组去除重复数据 2021-06-24
throws java_基于Java中throw和throws的区别(详解) 2021-06-24
java 多线程 临界区_【Java并发性和多线程】竞态条件与临界区 2021-06-24
java spring server_Java server框架之(1):spring中的IoC 2021-06-24
about java_About.java 2021-06-24
java hs_err 路径_JVM致命错误日志(hs_err_pid.log)解读 2021-06-24
java数字时钟控件_Java-数字时钟(简易版) 2021-06-24
python回到首行_python读取文件首行和最后一行 2021-06-24
java 全局变量 局部变量的区别_java中全局变量和局部变量的区别是什么? 2021-06-24
rust蓝卡怎么开_Rust娘个人资料简介,角色作品介绍 2021-06-24
将10个成绩排序java程序_快速排序——成绩排序 2021-06-24