设计原则之里氏替换原则

大家好,我是Ant,今天我们来学习的是设计原则之一的里氏替换原则(Liskov Substitution principle)

什么是里氏替换原则?

里氏替换原则是面向对象设计的基本原则之一,它是对继承复用的一个补充。

里氏替换原则的定义是:如果对于一个对象o1,都有对象o2,使得以o2替换o1,程序的行为没有发生变化,那么o2是o1的子类型。

简单来说,就是一个父类可以被它的子类所替换,而不会影响业务逻辑和正确性。

同时根据上面的定义,我们可以扩展一下:如果子类不能完整的实现父类的业务, 那么建议断开父子关系, 采用依赖、聚集、组合等方式代替继承。

为什么使用里氏替换原则?

里氏替换原则主要是来约束继承的复用的。

那么为什么要使用呢?假设一下:

Order中有一个方法orderSortByAmount ,这个方法的功能是对订单根据金额进行排序, 而在OrderExt中将orderSortByAmoount 改成了根据订单的创建时间排序。那么使用的时候,既不报错,结果又不对,是不是就傻眼了。

所以,我们需要使用里氏替换原则来约束继承的复用。

违反里氏替换原则的情况

一般来讲,我们写的继承代码基本不会违背里氏替换原则,那么什么情况下会违背呢?

  • 第一种就是上面提到的,彻底修改了父类的业务逻辑。

  • 第二种是修改了父类的接口权限。不过这种情况IDEA编译器自动给避免了。

  • 第三种是子类违反了父类要求的入参、出参、异常等。

  • 第四种是子类违背了父类要求的特殊规则。

    • 比如父类要求不能出现负的金额,但子类却允许出现负的金额。

示例

在上一篇开闭原则中,我们有以下的示例,当时提到了这样修改违背了里氏替换原则

scss 复制代码
public class GoodsDiscounts extends Goods {
 GoodsDiscounts(String name, Double price) {
  super(name, price);
 }

 public Double getPrice() {
  super.getPrice() * 0.6;
 }
 public Double getOriginPrice() {
  super.getPrice();
 }
}

那么为什么说它违背了呢? 是否还记得,我们有这么一个方法:

arduino 复制代码
 public static Double getTotalPrice(Goods goods, int num) {
  return goods.getPrice() * num;
 }

这么看其实并没有违背吖?那么为什么说它违背呢? 如果我们将getTotalPrice定义为获取总价格,然后有getPayPrice()获取实际支付价格,代码如下:

arduino 复制代码
    public static Double getPayPrice(Goods goods, int num) {
        return goods.getPrice() * num;
    }

那么将GoodsDiscounts传入getTotalPrice中是否还能得到正确的结果呢? 明显我们得不到正确的结果,那么我们将GoodsDiscounts代码修改为如下:

scss 复制代码
public class GoodsDiscounts extends Goods {
 GoodsDiscounts(String name, Double price) {
  super(name, price);
 }
    
 public Double getDiscountsPrice() {
  super.getPrice() * 0.6;
 }
}

这样就没有修改getPrice()了,也没修改父类的业务逻辑,现在总该符合了吧?

其实还不符合,我们在getPayPrice()中就拿不到想要的结果了,要么就得修改getPayPrice方法,而这又不符合开闭原则。

那么我们就没有两全其美的方法吗? 其实在上面的示例我们可以感觉到GoodsDicounts似乎就不适合继承了。

这时候我们可以使用策略模式 来进行重构

首先建立一个策略, 先创建一个价格策略接口类,然后实现对应的策略:

java 复制代码
/**
 * 价格策略
 */
public interface PriceStrategy{
    Double calculatePrice(Double price, int num);
}

/**
 * 原价计算策略
 */
public PriceCalculatorStrategy implements PriceStrategy{
    @Override
    public Double calculatePrice(Double price, int num) {
        return price * num;
    }
}

/**
 * 折扣计算策略
 */
public DiscountsPriceCalculatorStrategy implements PriceStrategy{
    
    private double discounts = 1.0;
    
    DiscountsPriceCalculator(double discounts) {
        this.discounts = discounts;
    }
    
    @Override
    public Double calculatePrice(Double price, int num) {
        return price  * num;
    }
}

其次将GoodsTest修改为:

csharp 复制代码
public class GoodsTest{
    public static void main(String[] args) {
        Goods goods = new Goods("苹果", 5.0);
        int num = 5;
        System.out.println("买了" + num + goods.getName() + ", " +
                "总价:" + calculatePrice(goods, num, 
                new PriceCalculatorStrategy()));
        // 如果要知道折扣实付金额
        System.out.println("打折,只需要付" + calculatePrice(goods, num, 
          new DiscountsPriceCalculatorStrategy(0.6)));
    }

    /**
     * 计算价格,理论上应该是策略的调用类,这里简化为方法
     */
    public static Double calculatePrice(Goods goods, int num, 
      PriceStrategy strategy) {
        return strategy.calculatePrice(goods.getPrice(), num);
    }
}

这样我们就将GoodsDiscounts的业务逻辑和计算逻辑分离了,并且将计算逻辑抽象出来了,这样就符合了里氏替换原则。 之后再增加什么计算规则,都可以增加对应的策略。

可能上面这样改,我们可能会产生一个疑问:这不是修改了getTotalPrice()getPayPrice()吗?

其实有时我们不能要求一开始就设计的很完美,设计要适合,比如一开始想到了价格会出现不同的算法,那么就可以使用策略模式, 但有时可能很久都不会修改价格,那么就没必要设计,不能因为未来很低的可能性,造成过度设计。

如果一开始以为出现修改价格的可能性很低,但后来就是出现了,那么就可以先扩展,扩展不了就重构,重构不算修改。

结语

里氏替换原则是面向对象设计的基本原则之一,它主要是用来约束继承。

同时我们要注意,设计要适度,根据经验判断大概率会出现的,那么不用想,直接上设计模式(而且大概率也用过对应的设计模式), 如果小概率才出现,而且可能影响进度,造成代码冗余,那就没必要设计。

接下来我们将学习单一职责原则(Single Responsibility Principle) ,敬请期待。

相关推荐
程序员侠客行几秒前
Spring事务原理 二
java·后端·spring
sjsjsbbsbsn37 分钟前
Spring Boot定时任务原理
java·spring boot·后端
计算机毕设指导61 小时前
基于Springboot学生宿舍水电信息管理系统【附源码】
java·spring boot·后端·mysql·spring·tomcat·maven
计算机-秋大田1 小时前
基于Spring Boot的兴顺物流管理系统设计与实现(LW+源码+讲解)
java·vue.js·spring boot·后端·spring·课程设计
大腕先生2 小时前
微服务环境搭建&架构介绍(附超清图解&源代码)
微服务·云原生·架构
文军的烹饪实验室3 小时前
处理器架构、单片机、芯片、光刻机之间的关系
单片机·嵌入式硬件·架构
羊小猪~~3 小时前
MYSQL学习笔记(九):MYSQL表的“增删改查”
数据库·笔记·后端·sql·学习·mysql·考研
猫头虎-人工智能3 小时前
NVIDIA A100 SXM4与NVIDIA A100 PCIe版本区别深度对比:架构、性能与场景解析
gpt·架构·机器人·aigc·文心一言·palm
阿里妈妈技术3 小时前
提效10倍:基于Paimon+Dolphin湖仓一体新架构在阿里妈妈品牌业务探索实践
架构
豌豆花下猫3 小时前
Python 潮流周刊#90:uv 一周岁了,优缺点分析(摘要)
后端·python·ai