贯穿设计模式第四话--里氏替换原则

Posted 最爱吃鱼罐头

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了贯穿设计模式第四话--里氏替换原则相关的知识,希望对你有一定的参考价值。

🥳🥳🥳 茫茫人海千千万万,感谢这一刻你看到了我的文章,感谢观赏,大家好呀,我是最爱吃鱼罐头,大家可以叫鱼罐头呦~🥳🥳🥳

从今天开始,将开启一个专栏,【贯穿设计模式】,设计模式是对软件设计中普遍存在(反复出现)的各种问题,所提出的解决方案,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。为了能更好的设计出优雅的代码,为了能更好的提升自己的编程水准,为了能够更好的理解诸多技术的底层源码, 设计模式就是基石,万丈高楼平地起,一砖一瓦皆根基。 ✨✨欢迎订阅本专栏✨✨

🥺 本人不才,如果文章知识点有缺漏、错误的地方 🧐,也欢迎各位人才们评论批评指正!和大家一起学习,一起进步! 👀

❤️ 愿自己还有你在未来的日子,保持学习,保持进步,保持热爱,奔赴山海! ❤️

💬 最后,希望我的这篇文章能对你的有所帮助! 🍊 点赞 👍 收藏 ⭐留言 📝 都是我最大的动力!

📃 前言回顾


​ 🔥【贯穿设计模式】第一话·设计模式初介绍和单一职责原则🔥

​ 🔥【贯穿设计模式】第二话·设计模式的七大原则之开闭原则🔥

​ 🔥【贯穿设计模式】第三话·设计模式的七大原则之依赖倒转🔥

在第三篇文章中,我们了解设计模式的七大原则中第三个原则:依赖倒转原则;

我们来回顾下,它的定义:高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象;依赖倒转原则要求我们在程序代码中传递参数时或在关联关系中,尽量引用层次高的抽象层类。

并且我们通过上学时每个学期可能上课的不同导致如果需要再学习一门新课程的需求导致代码的修改等问题,值得注意的是:在实现依赖倒转原则时,需要针对抽象层编程,而将具体类的对象通过依赖注入的方式注入其他对象中。依赖注入是指当一个对象要与其他对象发生依赖关系时,通过抽象来注入所依赖的对象。而常用的注入方式有3种:接口注入、构造注入和Setter注入。

⭐ 前提需要

在面向对象的语言中,继承是必不可少的,它主要有以下几个优点:

  • 代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性;
  • 提高代码的可重用性;
  • 提高代码的可扩展性;
  • 提高产品或项目的开放性。

相应的,继承也存在缺点,主要体现在以下几个方面:

  • 继承是入侵式的。只要继承,就必须拥有父类的所有属性和方法;
  • 降低代码的灵活性。子类必须拥有父类的属性和方法,使子类受到限制;
  • 增强了耦合性。当父类的常量、变量和方法修改时,必须考虑子类的修改,这种修改可能造成大片的代码需要重构。
  • 从整体上看,继承的“利”大于“弊”,然而如何让继承中“利”的因素发挥最大作用,同时减少“弊”所带来的麻烦,这就需要引入“里氏替换原则”。

🍊 里氏替换原则

今天我们学习的是里氏替换原则,任何基类可以出现的地方,子类一定可以出现。

🫓 概述

  • 该原则是指任何基类可以出现的地方,子类一定可以出现,即所有引用基类的地方都必须能够透明的使用其子类;里氏替换原则是继承与复用的基石,只有当子类可以替换掉基类,且系统的功能不受影响时,基类才能被复用,而子类也能够在基础类上增加新的行为;所以里氏替换原则指的是任何基类可以出现的地方,子类一定可以出现;

  • 里氏替换原则是对开闭原则的补充,实现开闭原则的关键步骤就是抽象化,而基类与子类的继承关系就是抽象化的具体实现,所以里氏替换原则是对实现抽象化的具体步骤的规范;

  • 简单理解就是子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法,如果子类强制要重写父类的方法,那么可以再抽象一个基类,为他们的公共父类,或采用依赖、组合、聚合的方式来实现;

  • 比如有一个基类A有个实现了的方法,其子类B、C等都可以完全替换A类来实现,但不能影响原有的代码功能,如果子类B、C需要重写父类的方法的话,就会导致子类B、C不能完全替换基类A来使用了,此时应该可以再抽象一个基类,为他们的公共父类,或采用依赖、组合、聚合的方式来实现。

🧇 特点

里氏替换原则是实现开闭原则的重要方式之一,通过里氏替换可以使系统有以下优点

  • 解决了继承中重写父类造成的可复用性变差的问题;
  • 是动作正确性的保证,即类的扩展不会给已有的系统引入新的错误,降低了代码出错的可能性;
  • 加强程序的健壮性,同时变更时可以做到非常好的兼容性,提高程序的维护性、可扩展性,降低需求变更时引入的风险。

🧀 问题引出

在上Java相关课程时,学习面向对象的时候,老师都会提动物、猫、狗之间的关系,都会说他们之间的抽象关系或者继承关系,那接下来就以动物的例子来形象的讲解里氏替换原则吧。

1. 定义一个鸟类Bird:

对于鸟类来说,我们对此的第一印象就是鸟类都是能飞的。

package com.ygt.principle.lsp;

/**
 * 新建一个鸟类
 *  鸟类有个方法是可以非的方法
 */
public class Bird 

    // 鸟的名字
    private String name;

    // 构造方法
    public Bird(String name) 
        this.name = name;
    

    public void fly()
        System.out.println(this.name + "开始飞啦~~~");
    

2. 定义一个百灵鸟类Lark并继承鸟类的飞行的方法:

package com.ygt.principle.lsp;

/**
 * 创建一个百灵鸟类,继承鸟类
 */
public class Lark extends Bird

    public Lark(String name) 
        super(name);
    

3. 建立一个测试类LiskovSubstitutionTest测试一下百灵鸟的飞行:

package com.ygt.principle.lsp;

/**
 * 测试里氏替换原则
 */
public class LiskovSubstitutionTest 

    public static void main(String[] args) 
        // 创建百灵鸟,并让其有飞的动作
        Lark lark = new Lark("百灵鸟");
        lark.fly();
    

4. 得到的结果:

百灵鸟开始飞啦~~~

5. 现在新建一个鸵鸟类Ostrich并继承鸟类:

package com.ygt.principle.lsp;

/**
 * 建立一个鸵鸟类,并继承鸟类
 */
public class Ostrich extends Bird
    public Ostrich(String name) 
        super(name);
    

6. 测试鸵鸟的飞行功能:

package com.ygt.principle.lsp;

/**
 * 测试里氏替换原则
 */
public class LiskovSubstitutionTest 

    public static void main(String[] args) 
        // 创建百灵鸟,并让其有飞的动作
        Lark lark = new Lark("百灵鸟");
        lark.fly();

        // 创建鸵鸟,测试其飞行功能
        Ostrich ostrich = new Ostrich("鸵鸟");
        ostrich.fly();
    

7. 得到的结果:

百灵鸟开始飞啦~~~
鸵鸟开始飞啦~~~

可是现在有个问题,我们知道普遍的鸟类动物都是善于飞翔的,但是也有些鸟类是不会飞行的,就如鸵鸟、企鹅一般的鸟类是不会飞行的,那此时我们在代码中将其继承鸟类,是不合理的,因为如果要在鸵鸟类中重写修改飞行方法的话,这就务必导致违反了里氏替换原则了,我们将不能使用鸵鸟类来替换成鸟类来使用了。下面我们就一起来看看解决方案吧。

🍕 解决方案

在上面的时候说过,当然我们也可以重写飞行的方法,使鸵鸟的飞行功能是无的,但是这就破坏了里氏替换原则,也会导致整个系统的可复用性变差;这时常用的解决方案就是取消原来的继承关系,重新设计他们之间的关系,即使原来的父类(鸟类)和子类(鸵鸟类)都继承一个更通俗的基类(动物类),这样原来的继承关系去掉,最后采用依赖,聚合,组合等关系代替。

1. 定义一个动物类Animal:

package com.ygt.principle.lsp;

/**
 * 定义一个动物类
 */
public class Animal 

    // 动物的名称
    public String name;

    public Animal(String name) 
        this.name = name;
    

2. 修改鸟类、鸵鸟类和测试类:

package com.ygt.principle.lsp;

/**
 * 新建一个鸟类
 *  鸟类有个方法是可以非的方法
 *  修改如下,继承自动物类
 */
public class Bird extends Animal 

    public Bird(String name) 
        super(name);
    

    public void fly()
        System.out.println(super.name + "开始飞啦~~~");
    

package com.ygt.principle.lsp;

/**
 * 建立一个鸵鸟类,并继承鸟类
 *  修改如下,不继承鸟类,改继承动物类
 */
public class Ostrich extends Animal

    // 如果还想使用鸟类的属性以及方法,可以采用依赖方式
    private Bird bird;

    public Ostrich(String name) 
        super(name);
    

    public void run()
        System.out.println(name + "开始奔跑****");
    

package com.ygt.principle.lsp;

/**
 * 测试里氏替换原则
 */
public class LiskovSubstitutionTest 

    public static void main(String[] args) 
        // 创建百灵鸟,并让其有飞的动作
        Lark lark = new Lark("百灵鸟");
        lark.fly();

        // 创建鸵鸟,测试其飞行功能
        Ostrich ostrich = new Ostrich("鸵鸟");
        ostrich.run();
    

得到的结果:

百灵鸟开始飞啦~~~
鸵鸟开始奔跑****

这样我们通过使原来的鸟类和鸵鸟都继承一个新的基类Animal类后,这就排除了修改鸟类中的飞行方法了,如果还需要使用鸟类中的功能,可以通过依赖、聚合,组合等关系代替。

而我们在实际编程中,常常会通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的几率非常大。

🌸 完结

相信各位看官看到这里大致都对设计模式中的其中一个原则有了了解吧,里氏替换原则指任何基类可以出现的地方,子类一定可以出现,即所有引用基类的地方都必须能够透明的使用其子类,里氏替换原则是继承与复用的基石,里氏替换原则是对实现抽象化的具体步骤的规范。

学好设计模式,让你感受一些机械化代码之外的程序设计魅力,也可以让你理解各个框架底层的实现原理。最后,祝大家跟自己能在程序员这条越走越远呀,祝大家人均架构师,我也在努力。 接下来期待第五话:接口隔离原则。 💪💪💪

文章的最后来个小小的思维导图:

🧐 本人不才,如有什么缺漏、错误的地方,也欢迎各位人才们评论批评指正!🤞🤞🤞

🤩 当然如果这篇文章确定对你有点小小帮助的话,也请亲切可爱的人才们给个点赞、收藏下吧,非常感谢!🤗🤗🤗

🥂 虽然这篇文章完结了,但是我还在,永不完结。我会努力保持写文章。来日方长,何惧车遥马慢!✨✨✨

💟 感谢各位看到这里!愿你韶华不负,青春无悔!让我们一起加油吧! 🌼🌼🌼

💖 学到这里,今天的世界打烊了,晚安!🌙🌙🌙

设计模式软件设计七大原则 ( 里氏替换原则 | 定义 | 定义扩展 | 引申 | 意义 | 优点 )





一、里氏替换原则定义



里氏替换原则定义 :

如果 对每一个 类型为 T1对象 o1 , 都有 类型为 T2对象 o2 ,

使得 以 T1 定义的 所有程序 P所有对象 o1替换成 o2 时 ,

程序 P行为 没有发生变化 ,

那么 类型 T2类型 T1子类型 ;


符号缩写说明 : T 是 类型 Type , o 是 对象 Object , P 是 程序 Program ;


通俗理解 :

T1 类 生成 o1 对象 ,

T2 类 生成 o2 对象 ,

开发的 程序 P使用了 T1 类型 , 使用 T1 创建了对象 o1 ,

将程序中 所有的 o1 都替换成 T2 o2 时 ,

程序 P 的行为 , 没有发生变化 ,

可以认为 T2 是 T1 的子类型 ;


T2 是 T1 的子类型 , T1 则是 T2 的父类 ;

里氏替换原则 是 继承复用 的基石 , 只有当 子类 可以 替换 父类 , 并且 软件功能不受影响 时 , 父类才能真正的被复用 , 子类也能在父类的基础上 增加新的行为 ;

里氏替换原则 是对 开闭原则 的补充 , 实现开闭原则的关键是 进行抽象 , 父类 和 子类 的继承关系 , 就是 抽象 的具体实现 ;





二、里氏替换原则定义扩展



里氏替换原则定义扩展 :

一个 软件实体 如果 适用于 一个父类的话 ,

一定适用于其子类 ,

所有 引用父类的地方 , 必须能 透明地 使用其子类的对象 ,

子类对象 能够 替换父类对象 , 而 程序逻辑不变 ;


通过继承深入理解里氏替换原则 :

抽象类父类中如果已经有实现好的方法 , 实际上 , 是在设定一系列的规范 和 契约 ,

父类不强制要求子类遵从这些契约 ,

但是如果子类任意修改父类的非抽象方法 ,

就会破坏整个继承体系 ,

里氏替换原则 明确反对 子类重写父类方法 ;


继承作为 面向对象 的特性之一 , 给设计程序时 , 带来了很大的便利 , 同时也 带来很多弊端 ;

如 : 使用继承 , 会给程序带来一些侵入性 , 降低可移植性 , 增加了对象间的耦合 ;

如果一个父类 被 很多子类继承 , 假设修改该父类 , 必须考虑所有的子类 , 否则会给系统引入未知风险 ;





三、里氏替换原则引申意义



子类 可以 扩展 父类的功能 , 但是绝对不能 改变 父类原有的功能 ;


子类 可以 实现 父类的 抽象方法 , 但是 不能 覆盖 父类的 非抽象方法 ;


子类中 可以 增加 自己特有的方法 ;


重载 ( 输入参数 宽松 ) : 子类的方法 重载 父类的方法 时 , 方法的前置条件 ( 输入参数 ) , 要比 父类方法的输入参数更宽松 ;

如 : 父类的参数是 HashMap , 如果要符合 里氏替换原则 , 子类如果重载父类方法 , 那么需要使用 Map 类型参数 ;
( 这里注意区分 重写 与 重载 , 重写是重写父类方法 , 重载是函数名相同 , 参数不同 )


重写 ( 返回值 严格 ) : 当 子类的方法 重写 / 重载 / 实现 父类的方法时 , 方法的 后置条件 ( 返回值 ) 比父类更严格或相等 ;

如 : 父类的返回值是 Map , 子类的相同的方法 是 Map 或 HashMap ;





四、里氏替换原则意义



里氏替换原则 要求很多 , 但是在程序中 , 如果不遵守 里氏替换原则 ,
尤其是关于 重载 ( 输入参数 宽松 ) 和 重写 ( 返回值 严格 ) , 都没有特别注意 ,
程序也可以正常运行 , 不会出现问题 ;
后果是在 需求变更 , 引入新功能 , 重构时 , 出问题的风险增加 ;
里氏替换原则只是一个约束 , 不是严格执行的标准 ;





五、里氏替换原则优点



里氏替换原则优点 :

  • 防止继承泛滥 :开闭原则 的一种体现 ;
  • 增强健壮性 : 如果满足 里氏替换原则 , 会 加强程序的健壮性 , 同时 变更时 , 可以做到非常好的 兼容性 , 提高程序的 维护性 , 扩展性 ; 降低需求变更时 引入的风险 ;

以上是关于贯穿设计模式第四话--里氏替换原则的主要内容,如果未能解决你的问题,请参考以下文章

Java设计原则—里氏替换原则(转)

面向对象编程原则(05)——里氏替换原则

面向对象编程原则(05)——里氏替换原则

手撸golang 架构设计原则 里氏替换原则

设计模式软件设计七大原则 ( 里氏替换原则 | 定义 | 定义扩展 | 引申 | 意义 | 优点 )

Java设计模式如何正确的使用继承?里氏替换原则的使用