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

你好,我是猿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,持续为你呈现更多干货!

原创好文

相关推荐
clk66071 小时前
SSM 框架核心知识详解(Spring + SpringMVC + MyBatis)
java·spring·mybatis
JohnYan3 小时前
Bun技术评估 - 04 HTTP Client
javascript·后端·bun
shangjg33 小时前
Kafka 的 ISR 机制深度解析:保障数据可靠性的核心防线
java·后端·kafka
青莳吖4 小时前
使用 SseEmitter 实现 Spring Boot 后端的流式传输和前端的数据接收
前端·spring boot·后端
我的golang之路果然有问题5 小时前
ElasticSearch+Gin+Gorm简单示例
大数据·开发语言·后端·elasticsearch·搜索引擎·golang·gin
Alan3165 小时前
Qt 中,设置事件过滤器(Event Filter)的方式
java·开发语言·数据库
小鹭同学_5 小时前
Java基础 Day28 完结篇
java·开发语言·log4j
椰椰椰耶6 小时前
[网页五子棋][匹配模块]实现胜负判定,处理玩家掉线
java·开发语言·spring boot·websocket·spring
on the way 1236 小时前
结构性设计模式之Flyweight(享元)
java·设计模式·享元模式
mldong6 小时前
我的全栈工程师之路:全栈学习路线分享
前端·后端