设计原则之里氏替换原则

大家好,我是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) ,敬请期待。

相关推荐
monkey_meng9 分钟前
【Rust中的迭代器】
开发语言·后端·rust
余衫马12 分钟前
Rust-Trait 特征编程
开发语言·后端·rust
monkey_meng16 分钟前
【Rust中多线程同步机制】
开发语言·redis·后端·rust
W Y17 分钟前
【架构-37】Spark和Flink
架构·flink·spark
Gemini199538 分钟前
分布式和微服务的区别
分布式·微服务·架构
paopaokaka_luck5 小时前
【360】基于springboot的志愿服务管理系统
java·spring boot·后端·spring·毕业设计
码农小旋风6 小时前
详解K8S--声明式API
后端
Peter_chq6 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
Yaml47 小时前
Spring Boot 与 Vue 共筑二手书籍交易卓越平台
java·spring boot·后端·mysql·spring·vue·二手书籍
小小小妮子~7 小时前
Spring Boot详解:从入门到精通
java·spring boot·后端