编程进阶网编程进阶网
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 01.面向对象六大原则
  • 02.单一职责原则详解
  • 03.开闭原则详细介绍
  • 04.里式替换原则介绍
  • 05.接口隔离原则介绍
  • 06.依赖倒置原则介绍
  • 07.迪米特原则介绍
  • 08.代码保持简单原则
  • 09.DRY原则简单介绍

04.里式替换原则介绍

目录介绍

  • 01.问题思考的分析
  • 02.里式替换原则描述
  • 03.如何理解里式替换原则
  • 04.电商案例演变过程
  • 05.鸟类飞行演变过程
  • 06.里氏替换优缺点
  • 07.里式替换原则总结

01.问题思考的分析

  1. 什么是里氏替换的原则,如何理解这一原则?
  2. 有那些场景满足里氏替换原则?它跟多态有何区别?

在面向对象编程中,继承是一种重要的机制,它允许我们创建一个类(子类)来继承另一个类(父类)的属性和行为。子类通过继承父类,可以重用父类的代码,并且可以添加或修改一些特定的行为。

然而,当使用继承时,必须确保子类可以无缝地替换父类,而不会破坏原有的程序功能。这就是里式替换原则的背景。

02.里式替换原则描述

里式替换原则的英文翻译是:Liskov Substitution Principle,缩写为 LSP。这个原则最早是在 1986 年由 Barbara Liskov 提出,他是这么描述这条原则的:If S is a subtype of T, then objects of type T may be replaced with objects of type S, without breaking the program。

在 1996 年,Robert Martin 在他的 SOLID 原则中,重新描述了这个原则,英文原话是这样的:Functions that use pointers of references to base classes must be able to use objects of derived classes without knowing it。

综合两者的描述,将这条原则用中文描述出来,是这样的:

子类对象(object of subtype/derived class)能够替换程序(program)中父类对象(object of base/parent class)出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏。

03.如何理解里式替换原则

里氏替换原则(Liskov Substitution Principle,LSP)是设计模式六大原则之一:

  1. 子类必须能够替换父类: 子类对象可以替换父类对象,程序的行为不会发生变化。
  2. 保证行为一致性: 子类在扩展父类功能的同时,不能改变父类原有的行为。

通俗地说,如果我们在程序中使用的是一个基类对象,那么在不修改程序的前提下,用它的子类对象替换这个基类对象,程序应该仍然可以正常运行。

04.一个错误案例演变

4.1 有缺陷的代码

假设我们在电商系统中设计了一个支付类Payment,有一个子类CreditCardPayment用于处理信用卡支付:

class Payment {
    public void pay(double amount) {
        // 支付逻辑
    }
}

class CreditCardPayment extends Payment {
    @Override
    public void pay(double amount) {
        if (amount > 1000) {
            throw new IllegalArgumentException("信用卡支付金额不能超过1000元");
        }
        // 信用卡支付逻辑
    }
}

此时,如果我们在系统中使用Payment基类对象进行支付:

Payment payment = new CreditCardPayment();
payment.pay(1200);

由于CreditCardPayment类中的逻辑限制,当支付金额超过1000元时会抛出异常。这导致了父类Payment的行为在子类CreditCardPayment中发生了变化,违反了里氏替换原则。

4.2 遵守里氏替换原则

为了遵循里氏替换原则,我们应该确保子类在扩展父类功能时,保持父类的行为一致性。可以通过在父类中添加必要的约束来确保子类行为的一致性:

class Payment {
    public void pay(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("支付金额必须大于0");
        }
        // 通用支付逻辑
    }
}

class CreditCardPayment extends Payment {
    @Override
    public void pay(double amount) {
        super.pay(amount);
        // 信用卡支付逻辑
    }
}

在这个设计中,CreditCardPayment类继承了父类Payment的行为,并且在支付逻辑之前调用了super.pay(amount),确保所有支付金额都符合父类的约束。

这样,无论是使用基类对象还是子类对象,程序的行为都保持一致,遵循了里氏替换原则。

05.鸟类飞行演变过程

5.1 未遵守里氏替换原则

例如:鸟一般都会飞行,如燕子的飞行速度大概是每小时 120 千米。但是新西兰的几维鸟由于翅膀退化无法飞行。

假如要设计一个实例,计算这两种鸟飞行 300 千米要花费的时间。显然,拿燕子来测试这段代码,结果正确,能计算出所需要的时间;但拿几维鸟来测试,结果会发生“除零异常”或是“无穷大”,明显不符合预期。

未遵守里氏替换原则:

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;
    }
}

这个设计存在的问题:

几维鸟类重写了鸟类的 setSpeed(double speed) 方法,这违背了里氏替换原则。

燕子和几维鸟都是鸟类,但是父类抽取的共性有问题,几维鸟的的飞行不是正常鸟类的功能,需要特殊处理,应该抽取更加共性的功能。

5.2 遵守里氏替换原则

取消几维鸟原来的继承关系,定义鸟和几维鸟的更一般的父类,如动物类,它们都有奔跑的能力。几维鸟的飞行速度虽然为 0,但奔跑速度不为 0,可以计算出其奔跑 300 千米所要花费的时间。

public class Lsptest2 {
    public static void main(String[] args) {
        Animal animal1 = new Bird();
        Animal animal2 = new BrownKiwi();
        animal1.setRunSpeed(120);
        animal2.setRunSpeed(180);
        System.out.println("如果奔跑300公里:");
        try {
            System.out.println("鸟类将奔跑" + animal1.getRunSpeed(300) + "小时.");
            System.out.println("几维鸟将奔跑" + animal2.getRunSpeed(300) + "小时。");
            Bird bird = new Swallow();
            bird.setFlySpeed(150);
            System.out.println("如果飞行300公里:");
            System.out.println("燕子将飞行" + bird.getFlyTime(300) + "小时.");
        } catch (Exception err) {
            System.out.println("发生错误了!");
        }
    }
}

/**
 * 动物类,抽象的功能更加具有共性
 */
class  Animal{
    Double runSpeed;

    public void setRunSpeed(double runSpeed) {
        this.runSpeed = runSpeed;
    }

    public double getRunSpeed(double distince) {
        return distince/runSpeed;
    }
}

/**
 * 鸟类继承动物类
 */
class Bird extends Animal{
    double flySpeed;

    public void setFlySpeed(double flySpeed) {
        this.flySpeed = flySpeed;
    }


    public double getFlyTime(double distince) {
        return distince/flySpeed;
    }
}


/**
 * 几维鸟继承动物类
 */
class  BrownKiwi extends  Animal{

}

/**
 * 燕子继承鸟类  飞行属于燕子的特性,
 */
class Swallow extends  Bird{

}

06.里氏替换优缺点

子类应该能够替代父类并且表现出相同的行为,而不需要修改原有的程序逻辑。这样可以确保代码的可扩展性、可维护性和可重用性。

遵循里式替换原则的好处包括:

  1. 代码的可扩展性:可以通过添加新的子类来扩展系统的功能,而不需要修改现有的代码。
  2. 代码的可维护性:当需要修改系统的行为时,只需要修改子类的代码,而不需要修改其他相关的代码。
  3. 代码的可重用性:可以通过使用父类的对象来处理子类的对象,从而提高代码的重用性。

它也有一些潜在的缺点和限制,包括:

  1. 过度设计:过度遵循LSP可能导致过度设计。为了确保子类能够无缝替换父类,可能需要在子类中添加许多条件和限制,这可能会增加代码的复杂性和维护成本。
  2. 难以满足所有情况:在某些情况下,很难设计出满足LSP的完美继承关系。特定的业务需求和复杂性可能导致无法完全满足LSP的要求,需要在设计中做出权衡和妥协。
  3. 可能引入不必要的复杂性:为了满足LSP,可能需要引入额外的抽象层次和接口,这可能增加代码的复杂性和理解难度。

07.里式替换原则总结

7.1 一些总结和分析

里氏替换原则与开闭原则的关系

里氏替换原则与开闭原则密切相关。开闭原则强调对扩展开放、对修改关闭,而里氏替换原则则确保子类能够正确地替换父类,使得扩展在不修改现有代码的情况下进行。

在电商交易系统中,遵循里氏替换原则可以确保我们在扩展支付方式、引入新的支付逻辑时,不会破坏已有系统的稳定性。例如,我们可以添加新的支付方式,而不影响原有的支付逻辑。

里式替换原则是用来指导,继承关系中子类该如何设计的一个原则

理解里式替换原则,最核心的就是理解“design by contract,按照协议来设计”这几个字。

父类定义了函数的“约定”(或者叫协议),那子类可以改变函数的内部实现逻辑,但不能改变函数原有的“约定”。

这里的约定包括:函数声明要实现的功能;对输入、输出、异常的约定;甚至包括注释中所罗列的任何特殊说明。

要弄明白里式替换原则跟多态的区别

虽然从定义描述和代码实现上来看,多态和里式替换有点类似,但它们关注的角度是不一样的。

  1. 多态是面向对象编程的一大特性,也是面向对象编程语言的一种语法。它是一种代码实现的思路。多态是指,子类可以替换父类,在实际的代码运行过程中,调用子类的方法实现。
  2. 里式替换是一种设计原则,用来指导继承关系中子类该如何设计,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑及不破坏原有程序的正确性。

7.2 开闭原则的总结

  1. 里式替换问题思考:什么是里氏替换的原则?有那些场景满足里氏替换原则?它跟多态有何区别?
  2. 如何理解里式替换原则:子类可以替换父类,并且保证原有的逻辑不变以及正确性不被破坏。
  3. 列举一个里氏替换的场景:比如支付宝,微信支付。将通用支付校验逻辑放到父类中,支付子类继承父类进行支付,支付金额都符合父类的约束。
  4. 里式替换原则的背景:面向对象中继承是一种机制,子类可以继承父类属性和行为。当使用继承时,子类不会破坏父类原有程序功能,这就是里氏替换的背景。
  5. 实现里式替换原则的方式:应该确保子类在扩展父类功能时,保持父类的行为一致性。简单说就是父类抽取通用的逻辑。
  6. 里式替换原则的案例教学:通用支付类中,有对金额进行校验,微信和支付宝支付子类通过继承父类,分别拓展自己的业务逻辑,且金额校验逻辑受到父类的约束。
  7. 里式替换原则的优点:子类替代父类行为,且不需要修改原有逻辑。可以确保代码可拓展,可维护,可重用等优点。
  8. 里式替换原则的缺点:存在过度设计,需要引入额外的抽象层次可能增加代码复杂性,等等。
  9. 总结如何理解里式替换原则:1.有继承关系;2.子类遵循父类约定;3.子类拓展行为不破坏父类行为。
  10. 里式替换原则跟多态的区别:多态是面向对象特性,是一个语法,子类可以替换父类实现。里氏替换是设计原则,是指子类设计要保证替换父类时,不改变原有逻辑和正确性!

08.更多内容推荐

模块描述备注
GitHub多个YC系列开源项目,包含Android组件库,以及多个案例GitHub
博客汇总汇聚Java,Android,C/C++,网络协议,算法,编程总结等YCBlogs
设计模式六大设计原则,23种设计模式,设计模式案例,面向对象思想设计模式
Java进阶数据设计和原理,面向对象核心思想,IO,异常,线程和并发,JVMJava高级
网络协议网络实际案例,网络原理和分层,Https,网络请求,故障排查网络协议
计算机原理计算机组成结构,框架,存储器,CPU设计,内存设计,指令编程原理,异常处理机制,IO操作和原理计算机基础
学习C编程C语言入门级别系统全面的学习教程,学习三到四个综合案例C编程
C++编程C++语言入门级别系统全面的教学教程,并发编程,核心原理C++编程
算法实践专栏,数组,链表,栈,队列,树,哈希,递归,查找,排序等Leetcode
Android基础入门,开源库解读,性能优化,Framework,方案设计Android

23种设计模式

23种设计模式 & 描述 & 核心作用包括
创建型模式
提供创建对象用例。能够将软件模块中对象的创建和对象的使用分离
工厂模式(Factory Pattern)
抽象工厂模式(Abstract Factory Pattern)
单例模式(Singleton Pattern)
建造者模式(Builder Pattern)
原型模式(Prototype Pattern)
结构型模式
关注类和对象的组合。描述如何将类或者对象结合在一起形成更大的结构
适配器模式(Adapter Pattern)
桥接模式(Bridge Pattern)
过滤器模式(Filter、Criteria Pattern)
组合模式(Composite Pattern)
装饰器模式(Decorator Pattern)
外观模式(Facade Pattern)
享元模式(Flyweight Pattern)
代理模式(Proxy Pattern)
行为型模式
特别关注对象之间的通信。主要解决的就是“类或对象之间的交互”问题
责任链模式(Chain of Responsibility Pattern)
命令模式(Command Pattern)
解释器模式(Interpreter Pattern)
迭代器模式(Iterator Pattern)
中介者模式(Mediator Pattern)
备忘录模式(Memento Pattern)
观察者模式(Observer Pattern)
状态模式(State Pattern)
空对象模式(Null Object Pattern)
策略模式(Strategy Pattern)
模板模式(Template Pattern)
访问者模式(Visitor Pattern)
贡献者: yangchong211
上一篇
03.开闭原则详细介绍
下一篇
05.接口隔离原则介绍