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

2023-05-16

在上一节《开闭原则》中,我们详细介绍了开闭原则,本节我们来介绍里式替换原则。

里氏替换原则的定义

里氏替换原则(Liskov Substitution Principle,LSP)由麻省理工学院计算机科学实验室的里斯科夫(Liskov)女士在 1987 年的“面向对象技术的高峰会议”(OOPSLA)上发表的一篇文章《数据抽象和层次》(Data Abstraction and Hierarchy)里提出来的,她提出:继承必须确保超类所拥有的性质在子类中仍然成立(Inheritance should ensure that any property proved about supertype objects also holds for subtype objects)。

里氏替换原则主要阐述了有关继承的一些原则,也就是什么时候应该使用继承,什么时候不应该使用继承,以及其中蕴含的原理。里氏替换原是继承复用的基础,它反映了基类与子类之间的关系,是对开闭原则的补充,是对实现抽象化的具体步骤的规范。

里氏替换原则的作用

里氏替换原则的主要作用如下。

  1. 里氏替换原则是实现开闭原则的重要方式之一。
  2. 它克服了继承中重写父类造成的可复用性变差的缺点。
  3. 它是动作正确性的保证。即类的扩展不会给已有的系统引入新的错误,降低了代码出错的可能性。
  4. 加强程序的健壮性,同时变更时可以做到非常好的兼容性,提高程序的维护性、可扩展性,降低需求变更时引入的风险。

里氏替换原则的实现方法

里氏替换原则通俗来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。

根据上述理解,对里氏替换原则的定义可以总结如下:

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

通过重写父类的方法来完成新的功能写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的概率会非常大。

如果程序违背了里氏替换原则,则继承类的对象在基类出现的地方会出现运行错误。这时其修正方法是:取消原来的继承关系,重新设计它们之间的关系。

关于里氏替换原则的例子,最有名的是“正方形不是长方形”。当然,生活中也有很多类似的例子,例如,企鹅、鸵鸟和几维鸟从生物学的角度来划分,它们属于鸟类;但从类的继承关系来看,由于它们不能继承“鸟”会飞的功能,所以它们不能定义成“鸟”的子类。同样,由于“气球鱼”不会游泳,所以不能定义成“鱼”的子类;“玩具炮”炸不了敌人,所以不能定义成“炮”的子类等。

下面以“几维鸟不是鸟”为例来说明里氏替换原则。

【例1】里氏替换原则在“几维鸟不是鸟”实例中的应用。

分析:鸟一般都会飞行,如燕子的飞行速度大概是每小时 120 千米。但是新西兰的几维鸟由于翅膀退化无法飞行。假如要设计一个实例,计算这两种鸟飞行 300 千米要花费的时间。显然,拿燕子来测试这段代码,结果正确,能计算出所需要的时间;但拿几维鸟来测试,结果会发生“除零异常”或是“无穷大”,明显不符合预期,其类图如图 1 所示。

“几维鸟不是鸟”实例的类图
图1 “几维鸟不是鸟”实例的类图

程序代码如下:

package principle;
public class LSPtest {
    public static void main(String[] args) {
        Bird bird1 = new Swallow();
        Bird bird2 = new BrownKiwi();
        bird1.setSpeed(120);
        bird2.setSpeed(120);
        System.out.println("如果飞行300公里:");
        try {
            System.out.println("燕子将飞行" + bird1.getFlyTime(300) + "小时.");
            System.out.println("几维鸟将飞行" + bird2.getFlyTime(300) + "小时。");
        } catch (Exception err) {
            System.out.println("发生错误了!");
        }
    }
}
//鸟类
class Bird {
    double flySpeed;
    public void setSpeed(double speed) {
        flySpeed = speed;
    }
    public double getFlyTime(double distance) {
        return (distance / flySpeed);
    }
}
//燕子类
class Swallow extends Bird {
}
//几维鸟类
class BrownKiwi extends Bird {
    public void setSpeed(double speed) {
        flySpeed = 0;
    }
}

程序的运行结果如下:

如果飞行300公里:
燕子将飞行2.5小时.
几维鸟将飞行Infinity小时。

程序运行错误的原因是:几维鸟类重写了鸟类的 setSpeed(double speed) 方法,这违背了里氏替换原则。正确的做法是:取消几维鸟原来的继承关系,定义鸟和几维鸟的更一般的父类,如动物类,它们都有奔跑的能力。几维鸟的飞行速度虽然为 0,但奔跑速度不为 0,可以计算出其奔跑 300 千米所要花费的时间。其类图如图 2 所示。

“几维鸟是动物”实例的类图
图2 “几维鸟是动物”实例的类图

进阶阅读

如果您想了解里氏替换原则在实际项目中的应用,可以猛击阅读《使用里氏替换原则解决实际问题》文章。

使用里氏替换原则解决实际问题

下面介绍一个经典的业务场景,用正方形、矩形和四边形的关系来说明里氏替换原则。

我们都知道正方形是一个特殊的长方形,首先创建一个长方形父类 Rectangle,代码如下。

public class Square extends Rectangle {
    private long length;
    public long getLength() {
        return length;
    }
    public void setLength(long length) {
        this.length = length;
    }
    @Override
    public long getHeight() {
        return getLength();
    }
    @Override
    public void setHeight(long height) {
        setLength(height);
    }
    @Override
    public void setWidth(long width) {
        setLength(width);
    }
    @Override
    public long getWidth() {
        return getLength();
    }
}

在测试类中,创建 resize() 方法。根据逻辑,长方形的宽应该大于等于高,我们让高一直自增,直到高等于宽变成正方形,代码如下。

public static void resize(Rectangle rectangle) {
    while (rectangle.getWidth() >= rectangle.getHeight()) {
        rectangle.setHeight(rectangle.getHeight() + 1);
        System.out.println("width: " + rectangle.getWidth() +
                ",Height: " + rectangle.getHeight());
    }
    System.out.println("Resize End,width:" + rectangle.getWidth() +
            " ,Height:" + rectangle.getHeight());
}

客户端测试代码如下:

public static void main(String[] args) {
    Rectangle rectangle = new Rectangle();
    rectangle.setWidth(20);
    rectangle.setHeight(10);
    resize(rectangle);
}

运行结果如下:

width: 20,Height: 11
width: 20,Height: 12
width: 20,Height: 13
width: 20,Height: 14
width: 20,Height: 15
width: 20,Height: 16
width: 20,Height: 17
width: 20,Height: 18
width: 20,Height: 19
width: 20,Height: 20
width: 20,Height: 21
Resize End,width:20 ,Height:21

由运行结果可知,高比宽还大,这在长方形中是一种非常正常的情况。再看下面的代码,把长方形替换成它的子类正方形,修改客户端测试代码如下:

public static void main(String[] args) {   
    Square square = new Square();
    square.setLength(10);
    resize(square);
}

此时,运行出现了死循环,违背了里氏替换原则,在将父类替换成子类后,程序运行结果没有达到预期。因此,代码设计是存在一定风险的。

里氏替换原则只存在于父类和子类之间,约束继承泛滥。再来创建一个基于长方形与正方形共同的抽象——四边形 QuardRangle 接口,代码如下。

public interface QuardRangle {
    long getWidth();
    long getHeight();
}

修改长方形 Rectangle 类的代码如下。

public class Rectangle implements QuardRangle{
    private long height;    //高
    private long width;    //宽
    public long getHeight() {
        return height;
    }
    public void setHeight(long height) {
        this.height = height;
    }
    public void setWidth(long width) {
        this.width = width;
    }
    public long getWidth() {
        return width;
    }
}

修改正方形 Square 类的代码如下。

public class Square implements QuardRangle {
    private long length;
    public long getLength() {
        return length;
    }
    public void setLength(long length) {
        this.length = length;
    }
    @Override
    public long getHeight() {
        return getLength();
    }
    @Override
    public long getWidth() {
        return getLength();
    }
}

此时,如果把 resize() 方法的参数换成四边形 QuardRangle 类,方法内部就会报错。因为正方形已经没有了 setWidth() 和 setHeight() 方法,所以,为了约束继承泛滥,resize() 方法的参数只能用长方形 Rectangle 类。

拓展

在讲开闭原则的时候,我们埋下了一个伏笔。在 JavaDiscountCourse 类中获取折扣价格时重写了父类的 getPrice() 方法,增加了一个获取源码的 getOriginPrice() 方法,这明显违背了里氏替换原则。

下面修改代码,增加 getDiscountPrice() 方法。JavaDiscountCourse 类代码如下:

public class JavaDiscountCourse extends JavaCourse {
    public JavaDiscountCourse(Integer id, String name, Double price) {
        super(id, name, price);
    }
    public Double getDiscountPrice() {
        return super.getPrice() * 0.8;
    }
}

转载于:http://c.biancheng.net/design_pattern/

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

里氏替换原则——面向对象设计原则 的相关文章

随机推荐

  • 搭建一个轻量级实验室,还不错

    转载于 xff1a https mp weixin qq com s m4 IwyEheiCne5oSr6 LwQ 几乎绝大部分电子工程师都有一个 梦想 xff0c 那就是拥有自己的实验室 有不少朋友问 xff0c 搭建一个电子实验室是不是
  • C语言类型转换-自动类型转换、强制类型转换、指针类型转换

    数据类型转换就是将数据 xff08 变量 数值 表达式的结果等 xff09 从一种类型转换为另一种类型 自动类型转换 自动类型转换就是编译器默默地 隐式地 偷偷地进行的数据类型转换 xff0c 这种转换不需要程序员干预 xff0c 会自动发
  • 【C/C++开源库】适合嵌入式的定时器调度器

    一 背景 在嵌入式软件中 xff0c 我们经常需要使用定时功能 xff0c 比如每1s执行某个功能 xff0c 比如触发了某个条件之后持续1s 如果每次遇到定时的功能 xff0c 我们都自己去计数 xff0c 这会让我们的代码很混乱 xff
  • 【C/C++开源库】环形队列,消息队列库

    一 环形队列库 之前介绍过一个环形缓冲库 xff1a C语言开源库 在CLion上使用一个轻量的适合嵌入式系统的环形缓冲库ring buffer 和C语言Unity单元测试框架 环形缓冲库的设置是非常灵活的 xff0c 可以根据实际项目的需
  • QT学习Github地址,git使用记录

    QT学习 之前有一些QT的文章 xff1a QT5基础教程 xff08 介绍 xff0c 下载 xff0c 安装 xff0c 第一个QT程序 xff09 上位机总结 这次我把之前学习过程中的一些QT项目上传到github了 xff0c 有需
  • 探索Android中的Parcel机制(上)

    一 xff0e 先从 Serialize 说起 我们都知道 JAVA 中的 Serialize 机制 xff0c 译成串行化 序列化 xff0c 其作用是能将数据对象存入字节流当中 xff0c 在需要时重新生成对象 主要应用是利用外部存储设
  • 如何写好github上的README

    项目名称 xff08 超大字体或者是图片形式 xff09 这里再写一句骚气又精准的话描述你的项目吧 上手指南 写几句这样的话概括接下来的内容 xff1a 以下指南将帮助你在本地机器上安装和运行该项目 xff0c 进行开发和测试 关于如何将该
  • 如何阅读源码汇总

    译文 xff1a 从源码中学习 xff08 阅读源码 xff0c 初学者的有效成长方式 xff09 目录前言 为什么我们需要读源码 站在巨人的肩膀上 解决困难问题 扩展你的边界 应该读什么样的源码 如何读源码 预先准备 流程与技巧 结合上下
  • 如何调整修改电脑COM口号

    在有的时候 xff0c 我们使用USB转串口 xff0c 操作系统识别出的COM口号并没有按COM1 xff0c COM2 xff0c COM3 这样的顺序 xff0c 而如果恰巧我们使用的串口调试助手不能自动识别COM口 xff0c 只能
  • 调试程序时有些语句不执行 且不可设置成断点-调试代码与直接运行结果可能不一致原因剖析

    在keil下 xff0c 出现一些语句不能被编译 xff0c 或者不能打断点情况 不能设置断点的检查步骤 调试程序时有些语句不执行 且不可设置成断点 调试代码与直接运行结果可能不一致原因剖析
  • 1553B总线通信协议

    1553B总线基础知识 MIL STD 1553B详细介绍与学习记录 xff08 一 xff09
  • 软件设计模式概述

    Java设计模式 xff1a 23种设计模式全面解析 xff08 超级详细 xff09 设计模式 xff08 Design Pattern xff09 是前辈们对代码开发经验的总结 xff0c 是解决特定问题的一系列套路 它不是语法规定 x
  • GoF 的 23 种设计模式的分类和功能

    设计模式有两种分类方法 xff0c 即根据模式的目的来分和根据模式的作用的范围来分 1 根据目的来分 根据模式是用来完成什么工作来划分 xff0c 这种方式可分为创建型模式 结构型模式和行为型模式 3 种 创建型模式 xff1a 用于描述
  • UML统一建模语言是什么?

    UML xff08 Unified Modeling Language xff0c 统一建模语言 xff09 是用来设计软件蓝图的可视化建模语言 xff0c 是一种为面向对象系统的产品进行说明 可视化和编制文档的标准语言 xff0c 独立于
  • UML类图及类图之间的关系

    在 UML 2 0 的 13 种图中 xff0c 类图 xff08 Class Diagrams xff09 是使用频率最高的 UML 图之一 类图描述系统中的类 xff0c 以及各个类之间的关系的静态视图 xff0c 能够让我们在正确编写
  • UML统一建模语言是什么?

    UML xff08 Unified Modeling Language xff0c 统一建模语言 xff09 是用来设计软件蓝图的可视化建模语言 xff0c 是一种为面向对象系统的产品进行说明 可视化和编制文档的标准语言 xff0c 独立于
  • 探索Android中的Parcel机制(下)

    上一篇中我们透过源码看到了 Parcel 背后的机制 xff0c 本质上把它当成一个 Serialize 就可以了 xff0c 只是它是在内存中完成的序列化和反序列化 xff0c 利用的是连续的内存空间 xff0c 因此会更加高效 我们接下
  • 如何正确使用设计模式?

    设计模式不是为每个人准备的 xff0c 而是基于业务来选择设计模式 xff0c 需要时就能想到它 要明白一点 xff0c 技术永远为业务服务 xff0c 技术只是满足业务需要的一个工具 我们需要掌握每种设计模式的应用场景 特征 优缺点 xf
  • 开闭原则——面向对象设计原则,使用开闭原则解决实际问题

    在软件开发中 xff0c 为了提高软件系统的可维护性和可复用性 xff0c 增加软件的可扩展性和灵活性 xff0c 程序员要尽量根据 7 条原则来开发程序 xff0c 从而提高软件开发效率 节约软件开发成本和维护成本 我们将在下面的几节中依
  • 里氏替换原则——面向对象设计原则

    在上一节 开闭原则 中 xff0c 我们详细介绍了开闭原则 xff0c 本节我们来介绍里式替换原则 里氏替换原则的定义 里氏替换原则 xff08 Liskov Substitution Principle xff0c LSP xff09 由