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



    全网最全最细的【设计模式】总目录,收藏起来慢慢啃,看完不懂砍我

    什么是里氏替换原则

    类对象(object of subtype/derived class)能够替换程序(program)中父类对象(object of base/parent class)出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏。

    里氏替换原则与多态的区别

    实现多态的条件:
    1.继承:必须要有子类继承父类的继承关系。
    2.重写:子类需要对父类中的一些方法进行重写,然后调用方法时就会调用子类重写的方法而不是原本父类的方法。
    3.向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法。

    多态的核心概念是向上转型,这是实现多态的核心。

    而里氏替换原则讲的是,子类不能改变父类原来的逻辑,子类完美继承父类的设计初衷,并做了增强。

    这两者的出发点是不同的。

    实例

    我们举个例子,LoginLog类继承了原来的Login类,并判断是否需要打印登录日志。

    
    public class Login{
    
      public Response doLogin(Request request) {
        // ...登录逻辑
      }
    }
    
    public class LoginLog extends Login {
      private Boolean openDebug;
    
      public LoginLog(Bookean openDebug) {
        this.openDebug= openDebug;
      }
    
      @Override
      public Response doLogin(Request request) {
        if (openDebug == true) {
          logger.log("xxxx进行了登录");
        }
        return super.sendRequest(request);
      }
    }
    
    public class Demo {    
      public void demoFunction(Login login) {    
        Reuqest request = new Request();
        //...省略设置request中数据值的代码...
        Response response = login.doLogin(request);
        //...省略其他逻辑...
      }
    }
    
    // 里式替换原则
    Demo demo = new Demo();
    demo.demofunction(new LoginLog(true););
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

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

    乍一看上去,这不就是多态嘛,其实,里氏替换原则跟多态的核心思想是不同的,我们继续往下看,假如说LoginLog的代码稍作修改,改成下面这个样子:

    public class LoginLog extends Login {
      private Boolean openDebug;
    
      public LoginLog(Bookean openDebug) {
        this.openDebug= openDebug;
      }
    
      @Override
      public Response doLogin(Request request) {
        if (openDebug == false) {
          throw new RuntimeException(...);
        }
        logger.log("xxxx进行了登录");
        return super.sendRequest(request);
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    LoginLog继承了Login之后,将doLogin核心代码做了一些改变,使其部分入参的响应结果与父类不同。虽然LoginLog并不会带来编译上的错误,但是在设计上,我们认为它不符合里氏替换原则的。

    多态和里式替换有点类似,但它们关注的角度是不一样的。多态是面向对象编程的一大特性,也是面向对象编程语言的一种语法。它是一种代码实现的思路。而里式替换是一种设计原则,是用来指导继承关系中子类该如何设计的,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑以及不破坏原有程序的正确性。

    哪些代码违背里氏替换原则

    子类修改了父类原有的功能逻辑

    比如说,订单排序,父类是按照时间正序排列,子类按照时间倒叙排列。
    比如说,父类查询出用户的机构,子类却给去掉了。
    比如说,父类买卖商品时不允许超额售卖,子类却允许了超额售卖

    子类违背父类的输入输出、异常的细节约定

    比如说,父类对于特定条件下输出为null,子类修改了这个逻辑,输出不为null了。
    比如说,父类对于输入一个正整数,抛出一个异常,子类将这个异常捕获了。

    总而言之

    只要是父类的一切输入、输出、异常、逻辑,子类有任何的修改,就是违背里氏替换原则。

    子类不能改变父类原来的逻辑,子类完美继承父类的设计初衷,并做了增强。

    里氏替换原则的意义

    一、改进已有实现。例如程序最开始实现时采用了低效的排序算法,改进时使用LSP(里氏替换原则)实现更高效的排序算法。
    二、指导程序开发。告诉我们如何组织类和子类(subtype),子类的方法(非私有方法)要符合contract。
    三、改进抽象设计。如果一个子类中的实现违反了LSP(里氏替换原则),那么是不是考虑抽象或者设计出了问题。

    里氏替换最终一句话还是对扩展开放,对修改关闭,不能改变父类的入参,返回,但是子类可以自己扩展方法中的逻辑。父类方法名很明显限定了逻辑内容,比如按金额排序这种,子类就不要去重写金额排序,改成日期排序之类的,而应该抽出一个排序方法,然后再写一个获取排序的方法,父类获取排序调用金额排序,子类就重写调用排序方法,获取日期排序。

    也是为了避免“二意性”,这里是只父类的逻辑和子类逻辑差别太多,读代码的人会感觉模棱两可,父类一套,子类一套,到底应该读哪种。感觉会混乱。

    总之就是,子类的重写最好是扩展父类,而不要修改父类。

    参考资料

    王争老师《设计模式之美》

  • 相关阅读:
    2022京东双十一全品类销售额变化情况一览:50%增长,50%下滑
    JSP EL关系运算符
    PAT A1018 Public Bike Management(Dijkstra + DFS)
    [附源码]Python计算机毕业设计SSM街舞公司管理系统(程序+LW)
    【vue3】01. 跟着官网学习vue3
    python:切分多个串联但单个内部按大小排列数据列表
    MYSQL--事务
    手写简易版flexible.js以及源码分析
    【C# Programming】类、构造器、静态成员
    【已解决】将一个2708行64列的在GPU上的张量z0矩阵保存下来,格式为csv
  • 原文地址:https://blog.csdn.net/A_art_xiang/article/details/127619668