首页 / 资讯中心 / 文章详情

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

【设计模式之美】SOLID 原则之三:里式替换(LSP)跟多态有何区别?如何理解LSP中子类遵守父类的约定 ★ FEATURED ARTICLE
文章目录一. 如何理解“里式替换原则”二. 哪些代码明显违背了 LSP三. 回顾一. 如何理解“里式替换原则”子类对象能够替换程序中父类对象出现的任何地方并且保证原来程序的逻辑行为不变及正确性不被破坏。里氏替换原则例子如下//1. 父类 Transporter 使用 HttpClient 来传输网络数据。//2. 子类 SecurityTransporter 继承父类 Transporter增加了额外的功能//支持传输 appId 和 appToken 安全认证信息。public class Transporter{private HttpClient httpClient;publicTransporter(HttpClient httpClient){this.httpClienthttpClient;}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.appIdappId;this.appTokenappToken;}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 requestnewRequest();//...省略设置request中数据值的代码...Response responsetransporter.sendRequest(request);//...省略其他逻辑...}}// 里式替换原则Demo demonewDemo();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按照协议来设计”这几个字。父类定义了函数的“约定”或者叫协议那子类可以改变函数的内部实现逻辑但不能改变函数原有的“约定”。这里的约定包括函数声明要实现的功能对输入、输出、异常的约定甚至包括注释中所罗列的任何特殊说明。参考《设计模式之美》–王争
阅读完成 · 觉得有帮助?
咨询建站