news 2026/11/3 1:12:36

【设计模式之美】SOLID 原则之三:里式替换(LSP)跟多态有何区别?如何理解LSP中子类遵守父类的约定

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【设计模式之美】SOLID 原则之三:里式替换(LSP)跟多态有何区别?如何理解LSP中子类遵守父类的约定

文章目录

    • 一. 如何理解“里式替换原则”?
    • 二. 哪些代码明显违背了 LSP?
    • 三. 回顾

一. 如何理解“里式替换原则”?

子类对象能够替换程序中父类对象出现的任何地方,并且保证原来程序的逻辑行为不变及正确性不被破坏。

里氏替换原则例子如下:

//>1. 父类 Transporter 使用 HttpClient 来传输网络数据。//>2. 子类 SecurityTransporter 继承父类 Transporter,增加了额外的功能,//支持传输 appId 和 appToken 安全认证信息。public class Transporter{private HttpClient httpClient;publicTransporter(HttpClient httpClient){this.httpClient=httpClient;}public ResponsesendRequest(Request request){// ...use httpClient to send request}}public class SecurityTransporter extends Transporter{private String appId;private String appToken;publicSecurityTransporter(HttpClient httpClient,String appId,String appToken){super(httpClient);this.appId=appId;this.appToken=appToken;}@Override public ResponsesendRequest(Request request){if(StringUtils.isNotBlank(appId)&&StringUtils.isNotBlank(appToken)){request.addPayload("app-id",appId);request.addPayload("app-token",appToken);}// ***************// 这里调用了父类的方法:即没有改变父类的逻辑(约定)// ***************returnsuper.sendRequest(request);}}public class Demo{publicvoiddemoFunction(Transporter transporter){Reuqest request=newRequest();//...省略设置request中数据值的代码...Response response=transporter.sendRequest(request);//...省略其他逻辑...}}// 里式替换原则Demo demo=newDemo();demo.demofunction(newSecurityTransporter(/*省略参数*/););

从刚刚的例子和定义描述来看,里式替换原则跟多态看起来确实有点类似,但实际上它们完全是两回事。

先改造下程序(改造成多态)

//改造前,如果appId 或者 appToken 没有设置,我们就不做校验;//改造后,如果 appId 或者 appToken 没有设置,则直接抛出NoAuthorizationRuntimeException 未授权异常。// 改造前:public class SecurityTransporter extends Transporter{//...省略其他代码..@Override public ResponsesendRequest(Request request){if(StringUtils.isNotBlank(appId)&&StringUtils.isNotBlank(appToken)){request.addPayload("app-id",appId);request.addPayload("app-token",appToken);}returnsuper.sendRequest(request);}}// 改造后:public class SecurityTransporter extends Transporter{//...省略其他代码..@Override public ResponsesendRequest(Request request){if(StringUtils.isBlank(appId)||StringUtils.isBlank(appToken)){throw newNoAuthorizationRuntimeException(...);}request.addPayload("app-id",appId);request.addPayload("app-token",appToken);returnsuper.sendRequest(request);}}

从设计思路上来讲,SecurityTransporter 的设计是不符合里式替换原则的,因为它改变了父类原有的规则,我们接下来讨论里氏替换中协议的具体含义。

二. 哪些代码明显违背了 LSP?

里式替换原则还有另外一个更加能落地、更有指导意义的描述,那就是“Design By Contract”,中文翻译就是“按照协议来设计”。

具体说明一下

  • 子类在设计的时候,要遵守父类的行为约定(或者叫协议)。父类定义了函数的行为约定,那子类可以改变函数的内部实现逻辑,但不能改变函数原有的行为约定。
  • 这里的行为约定包括:函数声明要实现的功能;对输入、输出、异常的约定;甚至包括注释中所罗列的任何特殊说明。
  • 实际上,定义中父类和子类之间的关系,也可以替换成接口和实现类之间的关系。

如下几个违反里式替换原则的例子,来说明约定的含义:

1.子类违背父类声明要实现的功能

父类中提供的 sortOrdersByAmount() 订单排序函数,是按照金额从小到大来给订单排序的,而子类重写这个 sortOrdersByAmount() 订单排序函数之后,是按照创建日期来给订单排序的。那子类的设计就违背里式替换原则。

2.子类违背父类对输入、输出、异常的约定,那子类的设计就违背里式替换原则,如下:

  • 在父类中运行出错的时候返回 null;获取数据为空的时候返回空集合(empty collection)。而子类重载函数之后,实现变了,运行出错返回异常(exception),获取不到数据返回 null。
  • 在父类中,某个函数约定,输入数据可以是任意整数,但子类实现的时候,只允许输入数据是正整数,负数就抛出,也就是说,子类对输入的数据的校验比父类更加严格
  • 在父类中,某个函数约定,只会抛出 ArgumentNullException 异常,那子类的设计实现中只允许抛出 ArgumentNullException 异常,任何其他异常的抛出。

3.子类违背父类注释中所罗列的任何特殊说明

父类中定义的 withdraw() 提现函数的注释是这么写的:“用户的提现金额不得超过账户余额……”,而子类重写 withdraw() 函数之后,针对 VIP 账号实现了透支提现的功能,也就是提现金额可以大于账户余额,那这个子类的设计也是不符合里式替换原则的。

三. 回顾

里式替换原则是用来指导,继承关系中子类该如何设计的一个原则。


  • 理解里式替换原则,最核心的就是理解“design by contract,按照协议来设计”这几个字。父类定义了函数的“约定”(或者叫协议),那子类可以改变函数的内部实现逻辑,但不能改变函数原有的“约定”。
  • 这里的约定包括:函数声明要实现的功能;对输入、输出、异常的约定;甚至包括注释中所罗列的任何特殊说明。

参考:《设计模式之美》–王争

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/3 1:10:50

【变量】spark(六)变量共享:累加器和广播变量

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/11/3 1:10:40

java 并发 - ThreadLocal详解:它是怎么做到的线程隔离

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华