当前位置: 首页 > article >正文

04.里氏替换原则(Liskov Substitution Principle)

暴论:一般的,如果一个富二代不想着证明自己,那么他一辈子都会衣食无忧。

一言

里氏替换原则想告诉我们在继承过程中会遇到什么问题,以及继承有哪些注意事项。


概述

这是流传较广的一个段子:

“一个坐拥万贯家财的富二代,他可以终日花天酒地,飞扬跋扈,跑车炸街,美女为伴,极尽荒唐之能事。只要他不想着证明自己比父亲强,让父辈的产业按既定的规则运转,那么他将一生衣食无忧。”

在这里插入图片描述
看似戏谑的言论实则透露出的是一种稳健的合理。在父辈足够优秀,后人的能力又并非出类拔萃的情况下,不打破既有的优秀机制无疑是最稳妥的选择。段子归段子,玩笑归玩笑。在软件设计中,我们会经常遇到父类子类的继承关系,这个看似荒唐又合理的原则实际上就是里氏替换原则的精髓

OO中的继承性

继承包含这样一层含义:父类中凡是已经实现好的方法,实际上是在设定规范和契约,虽然它不强制要求所有的子类必须遵循这些契约,但是如果子类对这些已经实现的方法任意修改,就会对整个继承体系造成破坏
继承在给程序设计带来便利的同时,也带来了弊端。比如使用继承会给程序带来侵入性,程序的可移植性降低,增加对象间的耦合性,如果一个类被其他的类所继承则当这个类需要修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子类的功能都有可能产生故障。
于是里氏替换原则提供了针对这种问题的规范。

何为里氏替换原则

里氏替换原则在1988年由麻省理工学院的**芭芭拉·里斯克夫(Barbara Liskov)**女士提出。
如果对每个类型为T1的对象O1,都有类型为T2的对象O2,使得以T1定义的所有程序P在所有的对象O1都代换成O2时,程序P的行为没有发生变化,那么类型T2是类型T1的子类型。换句话说,所有引用基类的地方必须能透明地使用其子类的对象。
在使用继承时,子类尽量不要重写父类的方法。继承实际让两个类的耦合性增强了,在适当的情况下可以通过聚合、组合、依赖来解决问题。


三寸反骨

我们还是从一个反骨仔的故事开始,为什么里氏替换原则不让重写父类方法?我就要重写父类方法!
反例

public class Story {
    public static void main(String[] args) {
        A a = new A();
        System.out.println("100-50 = "+a.func1(100,50));
        System.out.println("100-200 = "+a.func1(100,200));

        System.out.println("-------------------------------");
        B b = new B();
        System.out.println("100-50 = "+b.func1(100,50));
        System.out.println("100-200 = "+b.func1(100,200));
        System.out.println("(100+200)*10 = "+b.func2(100,200));
    }
}

class A{
    public int func1(int a, int b){
        return a-b;
    }
}

class B extends A{
    public int func1(int a, int b){
        return a+b;
    }

    public int func2(int a, int b){
        return func1(a, b)*10;
    }
}

当反骨仔随心所欲的继承重写之后:
在这里插入图片描述
我们发现原本运行正常的相减功能发生了错误。原因就是类B无意中重写了父类的方法,造成原有功能出现错误。在实际编程中,我们常常会通过重写父类的方法完成新的功能,这样写起来虽然简单,但整个继承体系的复用性会比较差。特别是运行多态比较频繁的时候。
子类过分的逆反使得代码极难维护,甚至随着代码量的扩张,真的是牵一发而动全身。


克己正心

听人劝,吃饱饭。反骨仔经过实践之后觉得这个继承之后的重写确实要慎重。那么究竟要如何去优化呢?
通用的做法是:原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉采用依赖,聚合,组合等关系代替。
在这里插入图片描述
于是经过深思熟虑的代码出现了:

public class Story {
    public static void main(String[] args) {
        A a = new A();
        System.out.println("100-50="+a.func1(100,50));
        System.out.println("100-200="+a.func1(100,200));
        System.out.println("---------------------------------");
        B b = new B();
        //因为B类不再继承A类,因此调用者,不会在func1求减法
        System.out.println("100+50="+b.func1(100,50));
        System.out.println("100+200="+b.func1(100,200));
        System.out.println("(100+200)*10="+b.func2(100,200));
        System.out.println("---------------------------------");
        System.out.println("使用组合依然可以使用到A的方法");
        System.out.println("100-50="+b.func3(100,50));
    }
}
class Base{
}
class A extends Base{
    public int func1(int num1,int num2){
        return num1-num2;
    }
}
class B extends Base {
    private A a = new A();
    public int func1(int a, int b) {
        return a + b;
    }
    public int func2(int a, int b) {
        return func1(a, b) * 10;
    }
    public int func3(int a, int b) {
        return this.a.func1(a, b);
    }
}

我们采用引入基类,子类组合的方式淡化反骨仔的继承关系,进而削弱A、B业务之间的冲突,在一定程度上解耦合。提高了灵活性的同时,也遵循了里氏替换原则。
在这里插入图片描述


里氏替换原则面向类与类之间的继承关系提出了设计规范,在一定程度上规避了业务设计上的杂糅,使得方法在继承关系中更纯粹,也使得设计在扩展方面具有更好的管理性。
事实上,与这个理论很相近的开闭原则才是所有设计的核心。关于开闭原则的拆解我们下次继续!


关注我,共同进步,每周至少一更!——Wayne


http://www.kler.cn/a/158184.html

相关文章:

  • 三、C语言常见概念
  • 利用document.write阻塞js文件加载
  • React都有哪些hooks?
  • git常用命令小记
  • 数据结构——希尔排序(详解)
  • 在Vue3中使用缓存组件keep-alive vue3缓存组件
  • XXL-Job详解(三):任务开发
  • java--抽象类的常见应用场景:模板方法设计模式
  • PTA 7-230 美好日子
  • 【电路笔记】-电阻器额定功率
  • 降本提效!阿里提出大模型集成新方法
  • JS 实现一键复制文本内容
  • 面向注解编程—Spring 注解看这一篇就够了
  • 蓝桥杯算法心得——想吃冰淇淋和蛋糕(dp)
  • 迅速认识什么是格雷码,格雷码和二进制怎么进行转换
  • 华纳云:Tomcat无法加载css和js等静态资源文件怎么解决
  • Oracle之ORA-29275: 部分多字节字符
  • Ubuntu 2204 安装libimobiledevice
  • PIC12F1572-I/SN国产低成本32位MCU替换
  • 操作系统复习总结——文件管理