SOLID设计原则:里式替换原则

简介: 本文详细介绍了SOLID设计原则中的Liskov替换原则(LSP),并通过实例解释了其核心概念:子类型应能在不破坏应用的情况下替换父类型。文章首先从科学定义出发,逐步引出LSP的实际意义,并通过经典的正方形与长方形代码示例展示了违反LSP的问题及其解决方案。接着,通过股票交易场景进一步说明了如何正确应用LSP。最后总结了LSP的重要性及其在软件开发中的应用技巧。

你好,我是猿java。

前面几篇文章,我们介绍了 SOLID原则的单一职责原则和开闭原则,单一职责描述的模块需要对一类行为负责,开闭原则描述的是对扩展开放,对修改关闭。今天我们就来聊聊SOLID的第三个原则:Liskov替换原则。

什么是里式替换原则?

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

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替换原则可以这样:

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的描述更加直接:

Subtypes must be substitutable for their base types.

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

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

如何实现Liskov替换原则?

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

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

// 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);
    }
}

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

// 计算面积
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),但实际输出为 44=16,违反了里氏替换原则。

如何解决这个 bad case呢?

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

// 基类
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来解决这个问题。

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

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

    }

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

    }
}

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

// 定义股票交易子类,定义股票特定的买卖动作逻辑
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){
   

    }
}

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

// 定义基金交易子类,定义基金特定的买卖动作逻辑
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了。

// 定义债券交易子类,定义债券特定的买卖动作逻辑
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,持续输出硬核文章。

目录
相关文章
|
安全 算法 物联网
MQTT 安全通信 SSL 双向认证 | 学习笔记
快速学习 MQTT 安全通信 SSL 双向认证
MQTT 安全通信 SSL 双向认证 | 学习笔记
|
30天前
|
人工智能 前端开发 数据可视化
HTML is the new Markdown:来自 Claude Code 团队的实践
AI Agent兴起后,Markdown因简洁易编辑成为默认输出格式。但Anthropic工程师Thariq提出:HTML正成为“新Markdown”——它通过CSS、交互元素、图表与响应式布局,显著提升信息密度与可读性,更适合PR评审、设计原型、技术报告等复杂场景。业界共识渐明:Markdown适合作为AI与开发者的轻量底稿,HTML则担当面向人类的展示与协作层。
397 3
HTML is the new Markdown:来自 Claude Code 团队的实践
|
3月前
|
安全 定位技术 API
个人用户必看!3 种准确查 IP 地址地理位置与运营商的实用方法
本文详解IP地理查询的原理与实操:解析IP“漂移”原因(动态分配、NAT、数据库滞后),对比在线网页、免费API、系统命令三种查询方式,并提供准确率提示与实用小贴士,助力用户快速定位IP归属地与运营商。
3680 1
|
6月前
|
人工智能 自然语言处理 搜索推荐
2025年11月,中国数字人平台介绍及应用场景全解析
2025年,数字人技术加速落地,凭借AI、图形学与多模态交互融合,广泛应用于金融、教育、政务等领域。
|
3月前
|
人工智能 Linux API
OpenClaw(Clawdbot)保姆级部署手册:阿里云无影云电脑+本店多系统搭建+免费大模型配置指南
2026年,OpenClaw(Clawdbot)作为基于MIT开源协议的本地优先AI智能体执行网关,凭借“能听懂指令、能动手执行”的核心优势,成为个人与中小企业实现AI自动化落地的优选工具。与传统对话式AI不同,OpenClaw更像是“能直接上手干活的员工”,无需手动干预即可完成文件管理、网页自动化、任务编排等全流程操作,真正实现“指令一出,万事落地”。
687 0
|
存储 人工智能 测试技术
DeepWiki:告别迷茫!AI轻松解析Github代码库
DeepWiki 的核心目标是帮助开发者快速理解复杂的代码仓库。无论是公共仓库还是私有项目,它都可以通过简单的操作生成类似 Wikipedia 的文档页面。
|
设计模式 Java API
设计模式-------------静态/动态代理模式(结构型设计模式)
本文详细介绍了代理模式,包括其定义、应用场景、UML类图、代码实现和实际例子,阐述了静态代理和动态代理的区别以及它们的优缺点,展示了如何通过代理模式来控制对目标对象的访问并增强其功能。
|
Cloud Native Docker 容器
免费Docker镜像服务
近期,一位博友分享了如何利用Cloudfare路由功能实现Docker镜像代理的方法。本文作者则选择了一种更为简便的方式,直接使用道客(DaoCloud)提供的Docker镜像代理服务,该服务已稳定运行半年以上,支持通过添加域名前缀或修改配置文件两种方式使用。
914 4
|
移动开发 前端开发 PHP
分享105个PHP源码,总有一款适合您
分享105个PHP源码,总有一款适合您
700 0
|
安全 网络协议 网络安全
【HTTPS】对称加密和非对称加密
【HTTPS】对称加密和非对称加密
363 0