设计模式 - 六大设计原则之LSP(里氏替换)

news2024/11/15 8:34:25

文章目录

  • 概述
  • 里氏替换原则问题由来
  • 里氏替换的原则
  • 里氏替换原则的作用
  • Case
  • Bad Impl
  • Better Impl
    • 抽象银行卡类
    • 储蓄卡实现类
    • 信用卡实现类
    • 单元测试
  • 小结

在这里插入图片描述


概述

里氏替换原则(Liskov Substitution Principle , LSP) 由麻省理工学院计算机科学西教授 Barbara Liskov 于1987年提出, 她提出: 继承必须确保超类所拥有的性质在子类中仍然成立。


里氏替换原则问题由来

有一功能 P1,由类 A 完成。

现需要将功能 P1 进行扩展,扩展后的功能为 P,其中P由原有功能 P1 与新功能 P2 组成。

新功能 P 由类 A 的子类 B 来完成,则子类 B 在完成新功能 P2 的同时,有可能会导致原有功能 P1 发生故障。


里氏替换的原则

如果S是T的子类型,那么所有T类型的对象都可以在不破坏程序的情况下被S类型的对象替换。

简单来说: 子类可以扩展父类的功能,但不能改变父类原有的功能。 也就是说,当子类继承父类时,除了添加新的方法完成新增功能外,尽量不要重写父类的方法。

上面这就话包括了四点含义:

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

里氏替换原则的作用

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

Case

关于里氏替换的场景,最有名的就是“正方形不是长方形” , 当然了还有一些关于动物的例子,比如鸵鸟、企鹅都是鸟,但是不会飞。 这样的例子可以非常形象的帮助我们理解里氏替换中关于两个类的继承不能破坏原有特性的含义。

这里我们用个银行卡的场景来描述一下:

储蓄卡、信用卡都可以消费,但信用卡不宜提现,否则产生高额利息。两个类

  • 储蓄卡类
  • 信用卡类

为了让信用卡可以使用储蓄卡的一些方法,选择由信用卡类继承储蓄卡类,讨论一下里氏替换原则产生的一些要点。


Bad Impl

【储蓄卡】

public class CashCard {

    private Logger logger = LoggerFactory.getLogger(CashCard.class);

    /**
     * 提现
     *
     * @param orderId 单号
     * @param amount  金额
     * @return 状态码 0000成功、0001失败、0002重复
     */
    public String withdrawal(String orderId, BigDecimal amount) {
        // 模拟支付成功
        logger.info("提现成功,单号:{} 金额:{}", orderId, amount);
        return "0000";
    }

    /**
     * 储蓄
     *
     * @param orderId 单号
     * @param amount  金额
     */
    public String recharge(String orderId, BigDecimal amount) {
        // 模拟充值成功
        logger.info("储蓄成功,单号:{} 金额:{}", orderId, amount);
        return "0000";
    }

    /**
     * 交易流水查询
     * @return 交易流水
     */
    public List<String> tradeFlow() {
        logger.info("交易流水查询成功");
        List<String> tradeList = new ArrayList<String>();
        tradeList.add("100001,100.00");
        tradeList.add("100001,80.00");
        tradeList.add("100001,76.50");
        tradeList.add("100001,126.00");
        return tradeList;
    }

}

在这里插入图片描述

在储蓄卡中包括三个方法: 提现、储蓄、交易流水查询, 这都是模拟储蓄卡的基本功能。

接下来我们通过继承储蓄卡的功能实现信用卡的服务。


【信用卡】

public class CreditCard extends CashCard {

    private Logger logger = LoggerFactory.getLogger(CashCard.class);

    @Override
    public String withdrawal(String orderId, BigDecimal amount) {
        // 校验
        if (amount.compareTo(new BigDecimal(1000)) >= 0){
            logger.info("贷款金额校验(限额1000元),单号:{} 金额:{}", orderId, amount);
            return "0001";
        }
        // 模拟生成贷款单
        logger.info("生成贷款单,单号:{} 金额:{}", orderId, amount);
        // 模拟支付成功
        logger.info("贷款成功,单号:{} 金额:{}", orderId, amount);
        return "0000";
    }

    @Override
    public String recharge(String orderId, BigDecimal amount) {
        // 模拟生成还款单
        logger.info("生成还款单,单号:{} 金额:{}", orderId, amount);
        // 模拟还款成功
        logger.info("还款成功,单号:{} 金额:{}", orderId, amount);
        return "0000";
    }

    @Override
    public List<String> tradeFlow() {
        return super.tradeFlow();
    }

在这里插入图片描述

信用卡的功能实现是在继承了储蓄卡后,进行方法的重写: 支付、还款。 其实交易流水可以复用,也可以不用重写这个。

那看看单元测试是如何使用的?

public class Test {

    private Logger logger = LoggerFactory.getLogger(ApiTest.class);

    @Test
    public void test_CashCard() {
        CashCard cashCard = new CashCard();
        // 提现
        cashCard.withdrawal("100001", new BigDecimal(100));
        // 储蓄
        cashCard.recharge("100001", new BigDecimal(100));
        // 交易流水
        List<String> tradeFlow = cashCard.tradeFlow();
        logger.info("查询交易流水,{}", JSON.toJSONString(tradeFlow));
    }

    @Test
    public void test_CreditCard() {
        CreditCard creditCard = new CreditCard();
        // 支付
        creditCard.withdrawal("100001", new BigDecimal(100));
        // 还款
        creditCard.recharge("100001", new BigDecimal(100));
        // 交易流水
        List<String> tradeFlow = creditCard.tradeFlow();
        logger.info("查询交易流水,{}", JSON.toJSONString(tradeFlow));
    }

}

这种继承父类方式的优点是复用了父类的核心逻辑功能, 但是也破坏了原有的方法。 此时继承父类实现的信用卡的类并不满足里氏替换的原则。 也就是说,此时的子类不能承担原父类的功能,直接给储蓄卡使用。


Better Impl

信用卡和储蓄卡在功能上有些许类似,在实际开发的过程中也有很多共同的可服用的属性及逻辑。

实现这样的类的最好的方式就是提取出一个抽象类 , 由抽象类定义所有卡的共同核心属性、逻辑, 把卡的支付和还款等动作抽象成正向和逆向操作。

抽象银行卡类

public abstract class BankCard {

    private Logger logger = LoggerFactory.getLogger(BankCard.class);

    private String cardNo;   // 卡号
    private String cardDate; // 开卡时间

    public BankCard(String cardNo, String cardDate) {
        this.cardNo = cardNo;
        this.cardDate = cardDate;
    }

    abstract boolean rule(BigDecimal amount);

    // 正向入账,+ 钱
    public String positive(String orderId, BigDecimal amount) {
        // 入款成功,存款、还款
        logger.info("卡号{} 入款成功,单号:{} 金额:{}", cardNo, orderId, amount);
        return "0000";
    }

    // 逆向入账,- 钱
    public String negative(String orderId, BigDecimal amount) {
        // 入款成功,存款、还款
        logger.info("卡号{} 出款成功,单号:{} 金额:{}", cardNo, orderId, amount);
        return "0000";
    }

    /**
     * 交易流水查询
     *
     * @return 交易流水
     */
    public List<String> tradeFlow() {
        logger.info("交易流水查询成功");
        List<String> tradeList = new ArrayList<String>();
        tradeList.add("100001,100.00");
        tradeList.add("100001,80.00");
        tradeList.add("100001,76.50");
        tradeList.add("100001,126.00");
        return tradeList;
    }

    public String getCardNo() {
        return cardNo;
    }

    public String getCardDate() {
        return cardDate;
    }
}

抽象类中提供了卡的基本属性(卡号、开卡时间)及 核心方法。
在这里插入图片描述

正向入账: 加钱
逆向入账: 减钱。

接下来我们继承这个抽象类,实现储蓄卡的功能逻辑


储蓄卡实现类

public class CashCard extends BankCard {

    private Logger logger = LoggerFactory.getLogger(CashCard.class);

    public CashCard(String cardNo, String cardDate) {
        super(cardNo, cardDate);
    }

    boolean rule(BigDecimal amount) {
        return true;
    }

    /**
     * 提现
     *
     * @param orderId 单号
     * @param amount  金额
     * @return 状态码 0000成功、0001失败、0002重复
     */
    public String withdrawal(String orderId, BigDecimal amount) {
        // 模拟支付成功
        logger.info("提现成功,单号:{} 金额:{}", orderId, amount);
        return super.negative(orderId, amount);
    }

    /**
     * 储蓄
     *
     * @param orderId 单号
     * @param amount  金额
     */
    public String recharge(String orderId, BigDecimal amount) {
        // 模拟充值成功
        logger.info("储蓄成功,单号:{} 金额:{}", orderId, amount);
        return super.positive(orderId, amount);
    }

    /**
     * 风险校验
     *
     * @param cardNo  卡号
     * @param orderId 单号
     * @param amount  金额
     * @return 状态
     */
    public boolean checkRisk(String cardNo, String orderId, BigDecimal amount) {
        // 模拟风控校验
        logger.info("风控校验,卡号:{} 单号:{} 金额:{}", cardNo, orderId, amount);
        return true;
    }

}

储蓄卡类继承抽象父类BankCard ,实现了核心的功能包括规则过滤rule、提现、储蓄 (super.xx), 以及新增的扩展方法:风险校控checkRisk.

这样的实现方式基本满足里氏替换的基本原则:既实现抽象类的抽象方法,又没有破坏父类中的原有方法。

接下来的信用卡类,既可以继承抽象父类,也可以继承储蓄卡类, 但无论那种实现方式,都需要遵从里氏替换原则,不可以破坏父类原有的方法。


信用卡实现类

public class CreditCard extends CashCard {

    private Logger logger = LoggerFactory.getLogger(CreditCard.class);

    public CreditCard(String cardNo, String cardDate) {
        super(cardNo, cardDate);
    }

    boolean rule2(BigDecimal amount) {
        return amount.compareTo(new BigDecimal(1000)) <= 0;
    }

    /**
     * 提现,信用卡贷款
     *
     * @param orderId 单号
     * @param amount  金额
     * @return 状态码
     */
    public String loan(String orderId, BigDecimal amount) {
        boolean rule = rule2(amount);
        if (!rule) {
            logger.info("生成贷款单失败,金额超限。单号:{} 金额:{}", orderId, amount);
            return "0001";
        }
        // 模拟生成贷款单
        logger.info("生成贷款单,单号:{} 金额:{}", orderId, amount);
        // 模拟支付成功
        logger.info("贷款成功,单号:{} 金额:{}", orderId, amount);
        return super.negative(orderId, amount);

    }

    /**
     * 还款,信用卡还款
     *
     * @param orderId 单号
     * @param amount  金额
     * @return 状态码
     */
    public String repayment(String orderId, BigDecimal amount) {
        // 模拟生成还款单
        logger.info("生成还款单,单号:{} 金额:{}", orderId, amount);
        // 模拟还款成功
        logger.info("还款成功,单号:{} 金额:{}", orderId, amount);
        return super.positive(orderId, amount);
    }

}

信用卡类在继承弗雷后,使用了公共的属性,即卡号、开卡时间, 同时新增了符合信用卡的新方法: loan、repayment, 并且在两个方法中都使用了抽象类的核心功能。

另外,信用卡中新增了自己的规则rule2 , 并没有破话储蓄卡中的校验方法rule .

以上的实现方式都遵循了里氏替换原则下完成的,即子类可以随时替代存储卡类。


单元测试

      @Test
    public void test_bankCard() {
        logger.info("里氏替换前,CashCard类:");
        CashCard bankCard = new CashCard("123456", "2023-01-01");
        // 提现
        bankCard.withdrawal("100001", new BigDecimal(100));
        // 储蓄
        bankCard.recharge("100001", new BigDecimal(100));
    }


在这里插入图片描述

【测试信用卡】

     @Test
    public void test_CreditCard(){
        CreditCard creditCard = new CreditCard("123456", "2023-01-01");
        // 支付,贷款
        creditCard.loan("100001", new BigDecimal(100));
        // 还款
        creditCard.repayment("100001", new BigDecimal(100));
    }


在这里插入图片描述

【测试信用卡替换储蓄卡】

    @Test
    public void test_bankCard() {
    		
        logger.info("里氏替换后,CreditCard类:");
        CashCard creditCard = new CreditCard("123456", "2023-01-01");
        // 提现
        creditCard.withdrawal("100001", new BigDecimal(1000000));
        // 储蓄
        creditCard.recharge("100001", new BigDecimal(100));
    }

在这里插入图片描述

可以看到,储蓄卡功能正常, 继承储蓄卡实现的信用卡的功能也正常。

同时,原有储蓄卡的功能可以由信用卡类支持,即 CashCard creditCard = new CreditCard


小结

继承作为面向对象的重要特征,虽然给程序的开发带来了极大的便利,但也引入了一些弊端。

继承的开发方式给代码带来了侵入性,可移植能力降低, 类之间的耦合度较高。 当对父类修改时,就要考虑一整套子类的实现是否有风险,测试成本较高。

里氏替换原则的目的是使用约定的方式,让使用继承后的代码具备更良好的扩展性和兼容性。

有些公司的代码规范中是不允许使用多层继承,尤其是一些核心服务的扩展。 而继承多数用在系统架构初期定义好的逻辑上或抽象出的核心功能里。 如果使用了继承,就一定要遵循里氏替换原则,否则会让代码出问题的概率大增。

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/176881.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MySQL两种引擎的索引

推荐看看这个视频&#xff0c;将两种引擎实现的索引原理说的比较清楚 4-2 索引概述 本文转载&#xff1a;想飞的盗版鱼的博客​​​​​​ mysql5.5之后都是用idb了 补充一个主键和索引的关系&#xff1a;Are You OK&#xff1f;主键就是聚集索引吗&#xff1f; 索引类型…

五个简单常用的系统命令

一、文件扫描修复 以管理员身份进入命令提示符&#xff0c;输入Sfc空格/scannow 二、使用Check disk命令检查并修复磁盘错误 1、开机时连续不断的点击F11键&#xff0c;选择&#xff08;疑难解答&#xff09;-选择&#xff08;高级选项&#xff09;-选择&#xff08;命令提示…

virtualbox运行Ubuntu系统

virtualbox安装 下载virtualbox VirtualBox 7.0.6版本下载 安装virtualbox 点击执行VirtualBox-7.0.6-155176-Win.exe即可&#xff0c; 直接点击"下一步" 直到 “完成” 导入以有的ova虚拟机文件 1&#xff09;点击导入 2&#xff09;选择下载的ova文件 3&a…

GD32F4——定时器(TIMERx)

一、概述 定时器的主要功能是用来计时&#xff0c;时间到达之后可以产生中断&#xff0c;提醒计时时间到&#xff0c;然后可以在中断函数中去执行功能。 GD32F450ZGT6共14个定时器&#xff0c;分别为 &#xff08;1&#xff09;高级定时器&#xff08;TIMERx&#xff0c;x0,…

LightGBM介绍

LightGBM介绍 GBDT是机器学习中一个长盛不衰的模型&#xff0c;其主要思想是利用弱分类器&#xff08;决策树&#xff09;迭代训练以得到最优模型&#xff0c;该模型具有训练效果好&#xff0c;不易过拟合等优点。GBDT不仅在工业界应用广泛&#xff0c;通常被应用于多分类、点…

【ONE·C || 数据存储】

总言 C语言&#xff1a;数据存储相关介绍。 文章目录总言1、基本数据类型介绍1.1、整体介绍1.2、各数据类型分别说明&#xff08;整型、浮点型、构造、指针、空&#xff09;2、整型在内存中的存储2.1、原码、反码、补码2.1.1、总体介绍2.1.2、char、short类型在内存中的存储范围…

顺序表的具体使用方法.数据解构(二)

前言 提示&#xff1a;文本为数据解构(一)后续补充文&#xff1a; 本文具体讲解顺序表的具体使用方法 提示&#xff1a;以下是本篇文 系列文章目录 第一章 数据解构(一) 文章目录 前言 系列文章目录 一、静态的使用方式 二、动态的使用方式 1.定义一个结构体 2.初始化 3.扩容…

【ArcGIS微课1000例】0060:ArcGIS打开las格式点云数据的方法

文章目录 1. 使用上下文菜单创建 LAS 数据集2. 使用地理处理工具创建 LAS 数据集3. 显示LAS数据集LAS 数据集是位于文件夹中的独立文件,并且引用 LAS 格式的激光雷达数据和用于定义表面特征的可选表面约束要素。ArcGIS中,可使用创建 LAS 数据集工具或 ArcCatalog 中文件夹的上…

23种设计模式(二十)——责任链模式【数据结构】

文章目录 意图什么时候使用责任链真实世界类比责任链模式的实现责任链模式的优缺点亦称:职责链模式、命令链、CoR、Chain of Command、Chain of Responsibility 意图 将链中的每一个节点看作是一个对象,每个节点处理的请求不同,且内部自动维护一个下一节点对象。当一个请求…

【OpenGL学习】camera and camera control

摄像机 游戏中的相机可以理解为与现实中的相机类似&#xff0c;可以捕获对应的游戏画面。Camera在游戏引擎中一般也会展示为现实中相机的模型&#xff0c;使用时有两种实现方式&#xff0c;一种以组件形式挂载在Character上&#xff0c;一种则是单独存在。通常来讲&#xff0c…

Flutter 基础-下

一、shared_preferences shared_preferences 是一个本地数据缓存库&#xff08;类似 AsyncStorage&#xff09; https://pub.dev/packages/shared_preferences 使用步骤 在 pubsepc.yaml 中添加 shared_preferences 依赖安装依赖&#xff08;pub get | flutter packages get |…

Git使用Merge和Rebase区别及心得技巧

git rebase命令常常因为江湖上关于它是一种Git魔法命令的名声而导致Git新手对它敬而远之&#xff0c;但是事实上如果一个团队能够正确使用的话&#xff0c;它确实可以让生活变得更简单。在这篇文章中我们会比较git rebase和经常与之相提并论的git merge命令&#xff0c;并且在真…

【回望2022,走向2023】一个双非二本非科班的学生的旅途

目录 1.自我介绍 2.高考与暑假 梦想 幻灭 决心 暑假 3.大一上学期 4.奋进之路 5.展望未来 1.自我介绍 我是一个双非本科的大一学生&#xff0c;在2023年的新春之际&#xff0c;借着CSDN的这次年度总结活动&#xff0c;来好好回顾一下&#xff0c;2022这个平凡却又不乏…

css 2D转换

文章目录一、什么是2D转换二、rotate() 方法&#xff08;旋转&#xff09;三、translate() 方法&#xff08;位移&#xff09;四、scale() 方法&#xff08;缩放&#xff09;五、skew() 方法 &#xff08;倾斜&#xff09;一、什么是2D转换 在二维空间下对元素进行移动、缩放、…

面试官问我有没有分布式系统开发经验,我一脸懵圈…

目录 从单块系统说起团队越来越大&#xff0c;业务越来越复杂分布式出现&#xff1a;庞大系统分而治之分布式系统所带来的技术问题一句话总结&#xff1a;什么是分布式系统设计和开发经验补充说明&#xff1a;中间件系统及大数据系统 前言 现在有很多Java技术方向的同学在找工…

深度学习网络---YOLO系列

深度学习网络—YOLO yolov1&#xff08;仅适用一个卷积神经网络端到端地实现检测物体的目的&#xff09; 首先将输入图片resize到448448&#xff0c;然后送入CNN网络&#xff0c;最后处理预测的结果得到检测的目标&#xff1b;yolov1的具体思想是将全图划分为SS的格子&#xf…

结构型模式-外观模式

1.概述 有些人可能炒过股票&#xff0c;但其实大部分人都不太懂&#xff0c;这种没有足够了解证券知识的情况下做股票是很容易亏钱的&#xff0c;刚开始炒股肯定都会想&#xff0c;如果有个懂行的帮帮手就好&#xff0c;其实基金就是个好帮手&#xff0c;支付宝里就有许多的基…

智能的本质不是数据算法算力和知识

编者按&#xff1a;人机之间未解决的大部分问题不是统计问题&#xff0c;而是统计概率分布外的问题。人是自然的&#xff0c;又不是自然的&#xff0c;还是社会的&#xff0c;人类和机器都可以作为认知的载体&#xff0c;但认知的性质是不同的&#xff0c;一个是生命的认知&…

GA6-BGSM/GPRS模块介绍

GA6-BGSM/GPRS模块简介GA6-B是一个4频的GSM/GPRS模块&#xff0c;工作的频段为&#xff1a;EGSM 900MHz、 GSM850MHz和DCS1800, PCS1900。GA6-B支持GPRS multi-slot class 10/ class 8&#xff08;可选&#xff09;和 GPRS 编码格式CS-1, CS-2, CS-3 and CS-4。模块的尺寸只有2…

SelectPdf for .NET 22.0 Crack

SelectPdf for .NET 是一个专业的 PDF 库&#xff0c;可用于创建、编写、编辑、处理和读取 PDF 文件&#xff0c;而无需在 .NET 应用程序中使用任何外部依赖项。使用此 .NET PDF 库&#xff0c;您可以实现丰富的功能&#xff0c;从头开始创建 PDF 文件或完全通过 C#/VB.NET 处理…