里氏替换原则,替换的依据是什么?

你好,我是猿java。

前面几篇文章,我们介绍了 SOLID原则的单一职责原则,开闭原则,接口隔离原则和依赖倒置原则,今天我们就来聊聊 SOLID的最后一个原则 Liskov替换原则。

什么是里式替换原则?

里式替换原则,Liskov substitution principle(简称LSP),它是以作者 Barbara Liskov(一位美国女性计算机科学家,对编程语言和分布式计算做出了开创性的贡献,于2008年获得图灵奖)的名字命名的,Barbara Liskov 曾在1987年的会议主题演讲"数据抽象"中描述了子类型:

text 复制代码
Let Φ(x) be a property provable about objects x of type T. Then Φ(y) should be true for objects y of type S where S is a subtype of T.

Liskov替换原则的核心:设Φ(x)是关于 T类型对象 x的可证明性质。那么对于 S类型的对象 y,Φ(y)应该为真,其中 S是 T的子类型。

这种科学的定义是不是过于抽象,太烧脑了?因此,在实际软件开发中的 Liskov替换原则可以这样:

text 复制代码
The principle defines that objects of a superclass shall be replaceable with objects of its subclasses without breaking the application.
That requires the objects of your subclasses to behave in the same way as the objects of your superclass.

该原则定义了在不破坏应用程序的前提下,超类的对象应该可以被其子类的对象替换,这就要求子类对象的行为方式与您的超类对象相同。

Robert C. Martin 对SLP的描述更加直接:

text 复制代码
Subtypes must be substitutable for their base types.

子类型必须可以替代它们的基本类型。

通过上面几个描述,我们可以把 LSP通俗的表达成:子类型必须能够替换其父类。

如何实现Liskov替换原则?

说起 Liskov替换原则的实现,就不得不先看一个著名的违反 LSP设计案例:正放形/长方形问题。尽管这个 case已经有点老掉牙,但是为了帮助理解,我们还是炒一次剩饭。

数学知识告诉我们:正方形是一种特殊的长方形,因此用 java代码分别定义 Rectangle(长方形) 和 Square(正方形)两个类,并且 Square继承 Rectangle,代码如下:

java 复制代码
// Rectangle(长方形)
public class Rectangle {
    private int length;
    private int width;
    public void setLength(double length) {
        this.length = length;
    }

    public void setWidth(double width) {
        this.width = width;
    }
}

// Square(正方形)
public class Square extends Rectangle {
    // 设置边长
    @Override
    public void setLength(double length) {
        super.setLength(length);
        super.setWidth(length);
    }

    @Override
    public void setWidth(double width) {
        super.setLength(width);
        super.setWidth(width);
    }
}

假设现在的需求是计算几何图形的面积,因此面积计算代码会如下实现:

java 复制代码
// 计算面积
public int area(){
    Rectangle r = new Square(); 
    // 设置长度
    r.setLength(3);
    // 设置宽度
    r.setWidth(4);
    
    r.getLength * r.getWidth = 3 * 4 = 12;
    
    // 正方形
    Rectangle r = new Rectangle();
    // 设置长度
    r.setLength(3); // Length=3, Width=3
    // 设置宽度
    r.setWidth(4); // Length=4, Width=4
    
    r.getLength * r.getWidth = 4 * 4 = 16;
}

在这个例子中,Square类重写了 setLength和 setWidth方法,以确保正方形的长度和宽度总是相等的。因此:假设 length=3,width=4

  • 对于长方形,面积 = length * width= 3 * 4 = 12,符合预期;
  • 然而,用 Square对象替换 Rectangle对象时,程序的行为发生了变化,本期望矩形的面积为12(3 * 4),但实际输出为 4*4=16,违反了里氏替换原则。

如何解决这个 bad case呢?

可以定义一个几何图形的接口,设定一个计算面积的方法,然后长方形、正方形都实现这个接口,实现各自的面积计算逻辑,整体思路如下:

java 复制代码
// 基类
public interface Geometry{ 
    int area();
}

public class Rectangle implements Geometry{
    private int length;
    private int width;
    public int area(){
       return length * width;
    }
}

public class Square implements Geometry{
    private int side;
    public int area(){
       return side * side;
    }
}

我们再来看一个 LSP使用的例子:

假设有一个股票交易的场景,而且需要支持债券、股票和期权等不同证券类型的多种交易类型,我们就可以考虑使用 LSP来解决这个问题。

首先,我们定义一个交易的基类,并且在基类中定义买入和卖出两个方法实现,代码如下:

java 复制代码
// 定义一个交易类
public class Transaction{
    // 买进操作
    public void buy(String stock, int quantity, float price){
        
    }

    // 卖出操作
    public void sell(String stock, int quantity, float price){
        
    }
}

接着,定义一个子类:股票交易,它和基类具有相同的买入和卖出行为,因此,在股票交易子类中需要重写基类的方法,代码如下:

java 复制代码
// 定义股票交易子类,定义股票特定的买卖动作逻辑
public class StockTransaction extends Transaction{

    @Override
    public void buy(String stock, int quantity, float price){
       
    }
    @Override
    public void sell(String stock, int quantity, float price){
       
    }
}

同样,定义一个子类:基金交易,它和基类具有相同的买入和卖出行为,因此,在基金交易子类中需要重写基类的方法,代码如下:

java 复制代码
// 定义基金交易子类,定义基金特定的买卖动作逻辑
public class FundTransaction extends Transaction{

    @Override
    public void buy(String stock, int quantity, float price){
       
    }
    @Override
    public void sell(String stock, int quantity, float price){
       
    }
}

同样,我们还可以定义了债券交易子类,债券交易和交易基类具有相同的行为:买入和卖出。所以只需要重写基类的方法,实现子类特定的实现就ok了。

java 复制代码
// 定义债券交易子类,定义债券特定的买卖动作逻辑
public class BondTransaction extends Transaction{

    @Override
    public void buy(String stock, int quantity, float price){
       
    }
    @Override
    public void sell(String stock, int quantity, float price){
        
    }
}

上述交易的案例,股票交易和基金交易子类替换基类之后,并没有破坏基类的买入卖出行为,更具体地说,替换的子类实例仍提供 buy()和 sell(),可以以相同方式调用的功能。这个符合LSP。

经过我们的抽象、分离和改造之后,Stock.updateStock()类就稳定下来了,再也不需要增加一个事件然后增加一个else if分支处理。这种抽象带来的好处也是很明显的:每次有新的库存变更事件,只需要增加一个实现类,其他的逻辑都不需要更改,当库存事件无效时只需要把实现类删除即可。

总结

Liskov替换原则扩展了OCP开闭原则,它描述的子类型必须能够替换其父类型,而不会破坏应用程序。因此,子类需要遵循以下规则:

  • 不要对输入参数实施比父类实施更严格的验证规则。
  • 至少对父类应用的所有输出参数应用相同的规则。

Liskov替换原则相对前面的单一职责和开闭原则稍微晦涩一些,因此在开发中容易误用,因此我们特别要注意类之间是否存在继承关系。

LSP不仅可以用在类关系上,也可以应用在接口设计中。

学习交流

文章总结不易,感谢帮忙点赞,收藏,或者关注公众号:猿java,持续为你呈现更多干货!

原创好文

相关推荐
梦想不只是梦与想2 分钟前
鸿蒙系统开发状态更新字段区别对比
android·java·flutter·web·鸿蒙
异常君4 分钟前
深入解析 InnoDB 死锁:从案例到方案,全流程透视指南
数据库·后端·mysql
我是谁的程序员4 分钟前
flutter写ios插件如何引入第三方库 flutter插件开发教程
后端
Java水解4 分钟前
解救应用启动危机:Spring Boot的FailureAnalyzer机制
spring boot·后端
Bug-Free生活30 分钟前
Go语言入门到入土——一、安装和Hello World
开发语言·后端·golang
PHASELESS41136 分钟前
Java堆结构深度解析:原理、实现与应用全指南
java·开发语言·数据结构
DreamBoat_Onism36 分钟前
JVM 内存调优
java·jvm·后端
顺天gie43 分钟前
idea中提高编译速度研究
java·spring boot·intellij-idea
lamdaxu1 小时前
Kafka的消费全流程
后端
异常君1 小时前
深入 JVM:线程池源码剖析与性能调优全攻略
java·jvm·后端