• 设计原则之【里式替换原则】


    设计原则是指导我们代码设计的一些经验总结,也就是“心法”;面向对象就是我们的“武器”;设计模式就是“招式”。

    以心法为基础,以武器运用招式应对复杂的编程问题。

    来吧,通过生活中一个小场景,一起系统学习这6大设计原则。

    SOLID原则--SRP单一职责原则

    SOLID原则--OCP开放封闭原则

    SOLID法则--LSP里式替换原则

    SOLID原则--ISP接口隔离原则

    SOLID原则--DIP依赖反转原则

    LOD迪米特法则

    实习生表妹上班又闯祸了

    表妹:今天上班又闯祸了😔

    我:发生什么事情啦?

    表妹:我不小心改了后端接口名的大小写,前端页面报错了


    你看,这不就类似我们软件开发中的里式替换原则嘛。

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

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

    实际上,里式替换原则还有一个更加能落地、更有指导意义的描述,那就是“按照协议来设计”。

    子类在设计的时候,要遵守父类的行为约定(或者叫协议)。父类定义了函数的行为约定,那子类可以改变函数内部实现逻辑(重写),但不能改变函数原有的行为约定。

    这里的行为约定包括:函数声明要实现的功能对输入、输出、异常的约定;甚至包含注释中所罗列的任何特殊说明

    前后端协商好的接口文档,就相当于“协议”。前端和后端都分别按照这个协议独立开发,具体的实现逻辑,是递归、动态规划还是贪心,由开发者决定。

    实际上,定义中父类和子类之间的关系,也可以替换成接口和实现类之间的关系。

    比如,父类Transporter使用org.apache.http库中的HttpClient类传输网络数据。子类SecurityTransporter继承父类Transporter,增加了额外的功能,支持传输appID和appToken安全认证信息。

    复制代码
     1 public class Transporter {
     2     private HttpClient httpClient;
     3     
     4     public Transporter(HttpClient httpClient) {
     5         this.httpClient = httpClient;
     6     }
     7     
     8     public Response sendRequest(Request request) {
     9         // ...use httpClient to send request
    10     }
    11 }
    12 13 public class SecurityTransporter extends Transporter {
    14     private String appID;
    15     private String appToken;
    16     
    17     public SecurityTransporter(HttpClient httpClient, String appID, String appToken) {
    18         super(httpClient);
    19         this.appID = appID;
    20         this.appToken = appToken;
    21     }
    22     
    23     @Override
    24     public Response sendRequest(Request request) {
    25         if (StringUtils.isNotBlank(appID) && StringUtils.isNotBlank(appToken)) {
    26             request.addPayload("app-id", appID);
    27             request.addPayload("app-token", appToken);
    28         }
    29         return super.sendRequest(request);
    30     }
    31 }
    32 33 public class Demo {
    34     public void demoFunction(Transporter transporter) {
    35         Request request = new Request();
    36         // ...省略设置request中数据值的代码...
    37         Response response = transporter.sendRequest(request);
    38         // ...省略其他逻辑...
    39     }
    40 }
    41 42 // 里式替换原则
    43 Demo demo = new Demo();
    44 demo.demoFunction(new SecurityTransporter(/*省略参数*/););
    复制代码

     

    在上面的代码中,子类SecurityTransporter的设计完全符合里式替换原则,可以替换父类出现的任何位置,并且原来代码的逻辑行为(传输网络数据)不变且正确性也没有被破坏。

    你可能会问,上面的代码设计,不就是简单利用了面向对象的多态特性吗?

    “里式替换原则”就是多态吗?

    里式替换原则,是实现开闭原则的重要方式之一,由于使用父类对象的地方可以使用子类对象,因此,在程序中尽量使用父类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。

    那么,“里式替换原则”就是多态吗?

    还是刚才那个例子,不过需要对SecurityTransporter类中sendRequest()函数稍加改造一下。改造前,我们不校验appID或者appToken是否设置;改造后,如果appID和appToken没有设置,则直接抛出NoAuthorizationRuntimeException未授权异常。改造前后的代码对比如下:

    复制代码
     1 // 改造前:
     2 public class SecurityTransporter extends Transporter {
     3     // ...省略其他代码...
     4     @override
     5     public Response sendRequest(Request request) {
     6         if (StringUtils.isNotBlank(appID) && StringUtils.isNotBlank(appToken)) {
     7             request.addPayload("app-id", appID);
     8             request.addPayload("app-token", appToken);
     9         }
    10         return super.sendRequest(request);
    11     }
    12 }
    13 14 // 改造后:
    15 public class SecurityTransporter extends Transporter {
    16     // ...省略其他代码...
    17     @override
    18     public Response sendRequest(Request request) {
    19         if (StringUtils.isBlank(appID) || StringUtils.isBlank(appToken)) {
    20             throw new NoAuthorizationRuntimeException(...);
    21         }
    22         request.addPayload("add-id", appID);
    23         request.addPayload("app-token", appToken);
    24         return super.sendRequest(request);
    25     }
    26 }
    复制代码

     

    你看,使用改造后的代码后,如果传进demoFunction()函数的是父类Transporter对象,那demoFunction()函数并不会有异常抛出,但如果传递给demoFunction()函数的是子类SecurityTransporter对象,那demoFunction()就有可能有异常抛出。

    尽管代码中抛出的是运行时异常,我们可以不在代码中显式地捕获处理,但子类替换父类传递进demoFunction函数之后,整个程序的逻辑行为就发生了改变。

    虽然从定义描述和代码实现上看,多态和里式替换有点类似,但是它们关注的角度是不一样的。

    多态是面向对象编程的一大特性,也是面向对象编程语言的一种语法,是一种代码实现的思路。

    而里式替换是一种设计原则,是用来指导继承关系中,子类该如何设计,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑以及不破坏原有程序的正确性。

    哪些代码明显违背了“里式替换原则”?

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

    父类中提供的sortOrderByAmount()订单排序函数,是按照金额从小到大来给订单排序的,而子类重写这个sortOrderByAmount()订单排序函数之后,是按照创建日期来给订单排序的。

    那么,这个子类的设计就违背了里式替换原则。

    • 子类违背父类对输入、输出、异常的约定

    在父类中,某个函数约定:运行出错的时候返回null;获取数据为空的时候返回空集合。而子类重载函数之后,实现变了,运行出错返回异常,获取不到数据返回null。

    那么,这个子类的设计就违背了里式替换原则。

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

    父类中定义的withdraw()提现函数的注释是这么写的:“用户的提现金额不得超过账户余额...”,而子类重写withdraw()函数之后,针对VIP账号实现了透支提现的功能,也就是提现金额可以大于账户余额。

    那么,这个子类的设计就违背了里式替换原则。

    实际上,你发现没有,里式替换原则是非常宽松的。判断子类的设计实现是否违背了里式替换原则,可以拿父类的单元测试去验证子类的代码。

    如果某些单元测试运行失败,就有可能说明,子类的设计实现没有完全遵守父类的约定,子类有可能违背了里式替换原则。

    总结

    里式替换原则就是子类完美继承父类的设计初衷,并做了增强(增加自己特有的方法)。

    大白话就是,可以青出于蓝胜于蓝,但是祖传的东西不能变。

    好啦,每个设计原则是否应用得当,应该根据具体的业务场景,具体分析。

    参考

    极客时间专栏《设计模式之美》

  • 相关阅读:
    软件工程——设计模式之创建型模式(单例模式、抽象工厂模式、建造者模式、工厂模式、原型模式。)
    深度优先搜索&广度优先搜索
    Unity编辑器扩展之自定义Inspector面板
    汇编语言ret与call指令
    indiegogo众筹
    【Linux】进程信号----(一篇带你熟知进程信号)
    如何保护 PHP Web 应用程序并防止攻击?
    03【远程协作开发、TortoiseGit、IDEA绑定Git插件的使用】
    腾讯Libpag动画库研究2(Pag实现原理)
    NLP之BM25:BM25算法的简介、相关库、案例应用之详细攻略
  • 原文地址:https://www.cnblogs.com/Gopher-Wei/p/15944334.html