Liskov替换原则

2024-05-12 01:32
文章标签 替换 原则 liskov

本文主要是介绍Liskov替换原则,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

Liskov替换原则

文章目录

  • Liskov替换原则
    • 案例引入
    • 如何理解 子类型可以替换 基类型 ?
    • 违反LSP的危害
    • 总结
    • 参考文档

今天我继续来说 软件设计的另一个原则, LSP原则

里氏代换原则 英文缩写: LSP , 全称: Liskov Substitution Principle

来源 : 它由芭芭拉·利斯科夫(Barbara Liskov)在1987年在一次会议上名为“数据的抽象与层次”的演说中首先提出.

子类型(sub_type) 必须能够替换掉它们的基类型(base type)

  • 若对每个类型S 的对象 o1, 都存在一个类型T的对象o2, 使得在所有针对T编写的程序P中, 用o1替换o2后,程序P行为功能不变, 则S是T的子类型 .

  • 子类型(sub_type ) 必须能够 替换掉 它们的基类型(base type)

这个原则 看起来理所当然, 子类继承于父类, 自然包含父类的所有的公开的方法与属性. 父类可以使用的地方,子类也 当然可以使用了. 看起来非常合理. 然而,在我们实际编程的过程中, 就有可能违反这个原则.

案例引入

来看下面的一个小例子

有一个类Animal 派生出子类 Bird ,Dog 这些子类. Animal 有一个 bark 叫的方法. 但是 子类Bird 也想有自己的bark的方法. 就是 不同的子类的bark 方法 不太一样,于是就有了下面的代码.


class Animal:def __init__(self, name):self.name = namedef bark(self):print('animal bark')return 'animal bark'class Bird(Animal):def __init__(self, name):super(Bird, self).__init__(name)def bark(self):print('Bird ji ji ji')return 'Bird ji ji ji'class Dog(Animal):def __init__(self, name):super(Dog, self).__init__(name)passdef bark(self):print('Dog wang wang wang')return 'Dog wang wang wang'if __name__ == '__main__':animal = Animal('animal')dog = Dog('dog')bird = Bird('bird')animal.bark()dog.bark()bird.bark()   

继承关系图如下:
请添加图片描述

继承的含义就是 子类中拥有一个父类的所有的公开的 属性,方法. 如下图:

请添加图片描述

每个子类中都有自己的bark 方法,即每个子类重写的了父类Animal 的方法. 下面来运行 这段代码

结果如下:

在这里插入图片描述

下面我们来思考一下 这个问题, 现在 有一个函数bark, 如下:


def bark(animal: Animal):"""对于 bark 方法来说 应该有理由相信  animal 叫的方法 返回 animal bark:param animal::return:"""assert animal.bark() == 'animal bark'

对于编写 bark函数的人来说, 只要是 Animal 对象,就应该 有我这样的 断言应该没有任何问题的.

想想 刚刚说的 LSP 原则 突然有一天, 我把Animal 的子类传入的bark 函数中

if __name__ == '__main__':animal = Animal('animal')dog = Dog('dog')bird = Dog('bird')bark(animal)bark(dog)

发现当传入dog 的时候 代码 就报错了.

在这里插入图片描述

LSP 原则 要求: 子类必须能够 替换掉 它们的基类 . 这里我传入了子类 发现代码 就崩溃了.

所以这样的代码编码 就是有问题的,或者不是那么好的 代码. 因为 这里子类并不能替换父类的角色. 父类可以正常运行的程序, 放入子类后,代码就崩溃了. 这就是 说 子类的行为 和父类的行为 有不一样的结果. 才导致的代码崩溃.

再来看一下 LSP 原则

所有引用基类(父类)的地方必须能透明地使用其子类的对象。通俗的说,子类可以扩展父类功能,但不能改变父类原有功能。

如何理解 子类型可以替换 基类型 ?

要求使用基类的程序P中, 把程序P中的所有基类换成 派生的子类对象, 代码运行的结果应该保持一样,不应该有任何的差异.

在我们理解 继承的关系的时候, 有时候 用 is-a 来辨别 是否用于继承. 比如: 人是动物, 狗狗是动物 , 因此 写代码的时候 我们就好使用继承 这种关系. 大部分情况下 这种方法 是没有问题的. 好像也很符合逻辑的. 但是有的时候 并不是这样的. 比如说 长方形 与正方形的关系 , 正方形 是长方形吗? 逻辑上来说 可以算是 正方形就是 一种特殊的长方形.

class Rectangle:"""长方形"""def __init__(self, width=0, height=0):self.width = widthself.height = heightdef set_width(self, width):self.width = widthpassdef set_height(self, height):self.height = heightpassdef area(self):return self.width * self.heightclass Square(Rectangle):"""正方形"""def set_width(self, width):super(Square, self).set_width(width)super(Square, self).set_height(width)passdef set_height(self, height):super(Square, self).set_width(height)super(Square, self).set_height(height)pass

类图关系如下:

在这里插入图片描述

现在有一个 编写 g 函数的小伙子 ,写了如下代码:


def g(r: Rectangle):""""""r.set_height(5)r.set_width(4)assert r.area() == 20, "assert error"

在g 的编写小伙子 认为, 长方形 就可以分别设置长和宽, 并且 通过设置 长和宽, 最后可以计算出面积.

我们开始跑这个 g函数, 发现 当传入 正放心的对象 就报错了. 结果不是20 了? 为什么呢?

if __name__ == '__main__':r = Rectangle()sqare = Square()g(r)g(sqare)

在编写g函数的小伙子, 如果传递Rectangle 的对象,就可以分别设置 长与宽 , 然后计算 面积, 此函数运行是没有问题的. 但是 如果 此时传入 Square 对象 就会断言错误. 所以 问题就在于编写g 函数的小伙子 认为 长与宽 是两个相互独立的变量, 两者不会相互有关联,有影响.

很显然 改变一个长方形的长 ,宽度可以不受影响. 这个假设是合理的. 若是传入Square对象, 这个时候 设置长的时候 宽度也被设置了. 两者是有关联的. 这就是问题, 也就是程序失败的原因.

你可能会对g 函数 asertion 进行争辩, g 函数应该不能假设 长与宽是独立变化的. 编写 g 函数的小伙子是不会同意这种说法的. 函数g 以 Rectangle为参数.并且确实有一些不变的性质的原理说明 明显适用于 Rectangle 类 . 其中一个不变的性质 就是 width 与 height 是独立变化,相互不应该影响. 所以 小伙子 完全 可以使用断言 认为 面积就是 长乘以宽 . 而恰好的是, Square 类 违反了这种不变性 , 所以才导致了assertion 失败.

所以 在LSP 原则下 这种设计是有问题的. 那我们有什么办法解决这个问题呢?

我们可以让这两个类 继承一个共同的类就好了.

# -*- coding: utf-8 -*-class Quadrangle:"""四边形"""def __init__(self, width=0, height=0):self.width = widthself.height = heightdef set_width(self, width):self.width = widthdef set_height(self, height):self.height = heightdef area(self):return self.width * self.heightclass Rectangle(Quadrangle):"""长方形"""class Square(Quadrangle):"""正方形"""def __init__(self, side=0):super(Square, self).__init__(side, side)# 拓展子类self.side = sidedef set_side(self, side):"""添加 方法:param side: 边长:return:"""self.side = sidedef my_area(self):"""use my_area 的方法来计算 面积:return:"""return self.side * self.sidedef g1(q: Quadrangle):""""""q.set_height(5)q.set_width(4)assert q.area() == 20, "assert error "if __name__ == '__main__':r = Rectangle()sqare = Square()g1(r)g1(sqare)pass

修改后的类图关系 如下:

注意这里我们并没有对父类做任何修改,只是子类Square 中添加了一个属性,并且添加了一个新的方法,这样就不会破坏原来的代码LSP原则

在这里插入图片描述

这里 就可以 实现了. 添加中间层 Quadrangle 然后在正方形里面添加 属性, 方法, 而不该修改 Quadrangle 的方法.

这个原则保证了 代码可维护性,可重用性,健壮性.

违反LSP的危害

子类行为不一样, 可能需要对不同的子类做不同处理

增加了产生bug 的可能性

总结

本来主要介绍了编码设计的一个重要的原则,LSP 这个LSP 原则 如下要求。

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

  • 子类可以有个性,有自己独特的方法,但是不要重写父类已经实现的方法,而是扩展父类的方法.

好,今天的分享差不多就到这里,如果有什么问题,可以一起留言讨论。

参考文档

细说 里氏替换原则 知乎

极客学院 里氏替换原则

分享快乐,留住感动. '2022-01-14 01:40:27' --frank

这篇关于Liskov替换原则的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/981224

相关文章

C#实现添加/替换/提取或删除Excel中的图片

《C#实现添加/替换/提取或删除Excel中的图片》在Excel中插入与数据相关的图片,能将关键数据或信息以更直观的方式呈现出来,使文档更加美观,下面我们来看看如何在C#中实现添加/替换/提取或删除E... 在Excandroidel中插入与数据相关的图片,能将关键数据或信息以更直观的方式呈现出来,使文档更

Java操作xls替换文本或图片的功能实现

《Java操作xls替换文本或图片的功能实现》这篇文章主要给大家介绍了关于Java操作xls替换文本或图片功能实现的相关资料,文中通过示例代码讲解了文件上传、文件处理和Excel文件生成,需要的朋友可... 目录准备xls模板文件:template.xls准备需要替换的图片和数据功能实现包声明与导入类声明与

JVM内存调优原则及几种JVM内存调优方法

JVM内存调优原则及几种JVM内存调优方法 1、堆大小设置。 2、回收器选择。   1、在对JVM内存调优的时候不能只看操作系统级别Java进程所占用的内存,这个数值不能准确的反应堆内存的真实占用情况,因为GC过后这个值是不会变化的,因此内存调优的时候要更多地使用JDK提供的内存查看工具,比如JConsole和Java VisualVM。   2、对JVM内存的系统级的调优主要的目的是减少

notepad++ 正则表达式多条件查找替换

基础语法参考: https://www.cnblogs.com/winstonet/p/10635043.html https://www.linuxidc.com/Linux/2019-05/158701.htm   通常情况下我们查找的内容和要被替换掉的内容是一样的,我们只需要使用正则表达式精确框定查找内容,替换直接输入要替换的内容即可。 但有时会比较复杂,查找的内容,只需要替换其中

shell脚本中变量中字符串替换的测试 /和//的区别

test_char=abbbcbbbf echo "bf:test_char = " $test_char test_char=${test_char/bbb/ddd} echo "af:test_char = " $test_char 输出: bf:test_char =  abbbcbbbf af:test_char =  adddcbbbf 只匹配第一个

springboot启动时替换配置参数

SpringBoot启动时配置参数替换 一.背景 SpringBoot项目启动的时候,在不使用配置中心等的前提下或者有公司强制使用指定的“密码箱”情况下,需要远程获取关键配置信息,比如数据库密码,则需要在项目启动前获取配置并且进行本地配置替换。 二.Demo实现 1.maven依赖 <dependencies><dependency><groupId>org.springframewor

水处理过滤器运行特性及选择原则浅谈

过滤属于流体的净化过程中不可缺的处理环节,主要用于去除流体中的颗粒物或其他悬浮物。水处理过滤器的原理是利用有孔介质,从流体中去除污染物,使流体达到所需的洁净度水平。         水处理过滤器的滤壁是有一定厚度的,也就是说过滤器材具有深度,以“弯曲通 道”的形式对去除污染物起到了辅助作用。过滤器是除去液体中少量固体颗粒的设备,当流体进入置有一定规格滤网的滤筒后,其杂质被阻挡,而

Java中等题-整数替换(力扣)

给定一个正整数 n ,你可以做如下操作: 如果 n 是偶数,则用 n / 2替换 n 。如果 n 是奇数,则可以用 n + 1或n - 1替换 n 。 返回 n 变为 1 所需的 最小替换次数 。 示例 1: 输入:n = 8输出:3解释:8 -> 4 -> 2 -> 1 示例 2: 输入:n = 7输出:4解释:7 -> 8 -> 4 -> 2 -> 1或 7 ->

SpringBoot 集成 SpirePDF 实现文本替换

SpirePDF 10.6.2 很强大,API 也封装的很好,使用的时候及其舒适。但是需要购买许可,不然有很大限制,最大的问题在于会添加水印,这就导致基本上用不了。有钱真好,真是嘴馋。 好在 SpirePDF 也有版本较老的免费版本,有查到一个 5.1.0。接下来附上使用代码 1、在 pom.xml 文件中添加他们的源 <!-- 使用 huawei / aliyun 的 Maven 源,提升

NIFI汉化_替换logo_二次开发_Idea编译NIFI最新源码_详细过程记录_全解析_Maven编译NIFI避坑指南001

由于需要对NFI进行汉化,以及二次开发,首先要下载源码以后编辑通过,NIFI的源码,项目非常多,编译过程中需要编译超过570个jar包,同时编译过程很慢需要30多分钟. 1.首先下载NIFI源码,根据需要下载对应版本: https://github.com/kemixkoo/orchsym-runtime/   首先介绍一下,这个是一个公司根据nifi进行定制开发的,已经汉化,但是不能商