前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >04.里式替换原则介绍

04.里式替换原则介绍

原创
作者头像
杨充
发布2025-03-05 12:32:22
发布2025-03-05 12:32:22
740
举报

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用于处理信用卡支付:

代码语言:java
复制
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基类对象进行支付:

代码语言:java
复制
Payment payment = new CreditCardPayment();
payment.pay(1200);

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

4.2 遵守里氏替换原则

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

代码语言:java
复制
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 千米要花费的时间。显然,拿燕子来测试这段代码,结果正确,能计算出所需要的时间;但拿几维鸟来测试,结果会发生“除零异常”或是“无穷大”,明显不符合预期。

未遵守里氏替换原则:

代码语言:java
复制
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 千米所要花费的时间。

代码语言:java
复制
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 更多内容推荐

模块

描述

备注

GitHub

多个YC系列开源项目,包含Android组件库,以及多个案例

博客汇总

汇聚Java,Android,C/C++,网络协议,算法,编程总结等

设计模式

六大设计原则,23种设计模式,设计模式案例,面向对象思想

Java进阶

数据设计和原理,面向对象核心思想,IO,异常,线程和并发,JVM

网络协议

网络实际案例,网络原理和分层,Https,网络请求,故障排查

计算机原理

计算机组成结构,框架,存储器,CPU设计,内存设计,指令编程原理,异常处理机制,IO操作和原理

学习C编程

C语言入门级别系统全面的学习教程,学习三到四个综合案例

C++编程

C++语言入门级别系统全面的教学教程,并发编程,核心原理

算法实践

专栏,数组,链表,栈,队列,树,哈希,递归,查找,排序等

Android

基础入门,开源库解读,性能优化,Framework,方案设计

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 04.里式替换原则介绍
    • 推荐一个好玩网站
    • 01.问题思考的分析
    • 02.里式替换原则描述
    • 03.如何理解里式替换原则
    • 04.一个错误案例演变
      • 4.1 有缺陷的代码
      • 4.2 遵守里氏替换原则
    • 05.鸟类飞行演变过程
      • 5.1 未遵守里氏替换原则
      • 5.2 遵守里氏替换原则
    • 06.里氏替换优缺点
    • 07.里式替换原则总结
      • 7.1 一些总结和分析
      • 7.2 里式替换原则总结
      • 7.3 更多内容推荐
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档