写点什么

04. 里式替换原则介绍

作者:杨充
  • 2025-03-05
    湖北
  • 本文字数:4891 字

    阅读完需:约 16 分钟

04.里式替换原则介绍

目录介绍

  • 01.问题思考的分析

  • 02.里式替换原则描述

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

  • 04.电商案例演变过程

  • 05.鸟类飞行演变过程

  • 06.里氏替换优缺点

  • 07.里式替换原则总结

推荐一个好玩网站

一个最纯粹的技术分享网站,打造精品技术编程专栏!编程进阶网


https://yccoding.com/


设计模式 Git 项目地址:https://github.com/yangchong211/YCDesignBlog


里式替换原则(LSP)是面向对象设计的重要原则之一,确保子类可以无缝替换父类而不破坏程序功能。本文详细介绍了 LSP 的定义、背景、理解方法及应用场景,通过电商支付和鸟类飞行案例展示了如何遵循 LSP,并分析了其优缺点。


LSP 强调子类应保持父类的行为一致性,有助于提高代码的可扩展性、可维护性和可重用性,但也可能导致过度设计。最后,对比了 LSP 与多态的区别,明确了 LSP 作为设计原则的重要性。

01.问题思考的分析

什么是里氏替换的原则,如何理解这一原则?


有那些场景满足里氏替换原则?它跟多态有何区别?


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


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

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. 里式替换原则跟多态的区别:多态是面向对象特性,是一个语法,子类可以替换父类实现。里氏替换是设计原则,是指子类设计要保证替换父类时,不改变原有逻辑和正确性!

7.3 更多内容推荐


用户头像

杨充

关注

还未添加个人签名 2018-07-30 加入

还未添加个人简介

评论

发布
暂无评论
04.里式替换原则介绍_杨充_InfoQ写作社区