Liskov替换原则

Liskov替换原则


今天我继续来说 软件设计的另一个原则, 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 = name

    def 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)

    pass

    def 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 = width
        self.height = height

    def set_width(self, width):
        self.width = width
        pass

    def set_height(self, height):
        self.height = height
        pass

    def area(self):
        return self.width * self.height


class Square(Rectangle):
    """
    正方形
    """

    def set_width(self, width):
        super(Square, self).set_width(width)
        super(Square, self).set_height(width)
        pass

    def 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 = width
        self.height = height

    def set_width(self, width):
        self.width = width

    def set_height(self, height):
        self.height = height

    def area(self):
        return self.width * self.height


class Rectangle(Quadrangle):
    """
    长方形
    """


class Square(Quadrangle):
    """
    正方形
    """

    def __init__(self, side=0):
        super(Square, self).__init__(side, side)
        # 拓展子类
        self.side = side

    def set_side(self, side):
        """
        添加 方法
        :param side: 边长
        :return:
        """
        self.side = side

    def my_area(self):
        """
        use my_area 的方法来计算 面积
        :return:
        """
        return self.side * self.side


def 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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值