设计原则——里氏替换原则

1.全称

  • liskov substitution principle
  • 缩写为:LSP

2.解释

  • 继承必须确保超类所拥有的性质在子类中仍然成立
  • 里氏替换原则主要阐述了有关继承的一些原则,也就是什么时候应该使用继承,什么时候不应该使用继承,以及其中蕴含的原理。
  • 里氏替换原是继承复用的基础,它反映了基类与子类之间的关系,是对开闭原则的补充,是对实现抽象化的具体步骤的规范

3.里氏替换原则的实现方法

  • 1.里氏替换原则通俗来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法

  • 2.类的继承原则:里氏替换原则常用来检查两个类是否为继承关系。在符合里氏替换原则的继承关系中,使用父类代码的地方,用子类代码替换后,能够正确的执行动作处理。换句话说,如果子类替换了父类后,不能够正确执行动作,那么他们的继承关系就是不正确的,应该重新设计它们之间的关系。

    class zhangsan:
    	def sing(self):
    	    print(f"张三唱歌五音不全.")
    
    class zhangsanson(zhangsan):
    	def dance(self):
    		print("张三儿子会跳舞")
    
    
    if __name__ == "__main__":
    	zs = zhangsan()
    	zs_son = zhangsanson()
    	# 调用张三唱歌的方法
    	zs.sing()
    	# 替换为张三儿子来调用依然可以,没有任何变化,说明符合继承
    	zs_son.sing()
    
  • 3.动作正确性保证:里氏替换原则对子类进行了约束,所以在为已存在的类进行扩展,来创建一个新的子类时,符合里氏替换原则的扩展不会给已有的系统引入新的错误。符合开闭原则

  • 4.如果通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的概率会非常大。

  • 5.如果程序违背了里氏替换原则,则继承类的对象在基类出现的地方会出现运行错误。这时其修正方法是:取消原来的继承关系,重新设计它们之间的关系。

1.场景:正方形不是长方形
class Rectangle:
    def __init__(self, width, height):
        self._width = width
        self._height = height

    @property
    def width(self):
        return self._width

    @width.setter
    def width(self, value):
        self._width = value

    @property
    def height(self):
        return self._height

    @height.setter
    def height(self, value):
        self._height = value

    @property
    def area(self):
        return self._height * self._width

    def __str__(self):
        return f"Width: {self.width}, Height: {self.height}"


class Square(Rectangle):
    def __init__(self, size):
        super().__init__(width=size, height=size)

    @Rectangle.width.setter
    def width(self, value):
        self._width = value
        self._height = value

    @Rectangle.width.setter
    def height(self, value):
        self._height = value
        self._width = value


def use_it(rc):
    w = rc.width
    rc.height = 10  # 修改宽为10
    expected = w * 10
    print(f"Expected an area of {expected}, got {rc.area}")


if __name__ == '__main__':
    #
    rc = Rectangle(15, 5)
    # # Expected an area of 150, got 150
    use_it(rc)

    # 正方形的长和宽是一致的,所以只需要设置一个值即可
    sq = Square(15)
    # Expected an area of 50, got 100
    use_it(sq)
2.结论
  • 父类Rectangle不能被子类Square替换,如果进行了替换就得不到预期结果。因此,Square类和Rectangle类之间的继承关系违反了里氏替换原则,它们之间的继承关系不成立,正方形不是长方形。
3.阐释
正方形不是长方形正方形是长方形也不是长方形,这样结论似乎就是个悖论。产生这种混乱的原因有两个:
  • 原因一:对类的继承关系的定义没有搞清楚。

    • 面向对象的设计关注的是对象的行为,它是使用“行为”来对对象进行分类的,只有行为一致的对象才能抽象出一个类来

    正方形在设置长度和宽度这两个行为上,与长方形显然是不同的

    长方形的行为:设置长方形的长度的时候,它的宽度保持不变,设置宽度的时候,长度保持不变

    正方形的行为:设置正方形的长度的时候,宽度随之改变;设置宽度的时候,长度随之改变。

    所以,如果我们把这种行为加到父类长方形的时候,就导致了正方形无法继承这种行为。我们“强行”把正方形从长方形继承过来,就造成无法达到预期的结果。

  • 原因二:设计要依赖于用户需求和具体环境。

    • 继承关系要求子类要具有基类全部的行为。这里的行为是指落在需求范围内的行为
    • 所有子类的行为功能必须和使用者对其父类的期望保持一致,如果子类达不到这一点,那么必然违反里氏替换原则
    • 比如鸵鸟和鸟:鸵鸟是否可以继承自鸟
      • 需求一:用户关注点是飞行,由于鸵鸟不会飞,所以此时来看就不符合继承关系
      • 需求二:用户关注点是爪子或者羽毛,此时就可以满足继承关系
4.违反里氏替换原则需重新设计关系
1.方式一
  • 创建一个新的抽象类或者接口,作为两个具体类的基类。将具体类A和B的共同行为转移到C中,从而解决A和B行为不一致的问题。
2.方式二
  • 将B到A的继承关系改为委托关系。具体参考组合/聚合复用原则。
3.方式一解决长方形不是正方形问题
  • 正方形和长方形的共同行为(getLength()、getWidth()方法)抽象并封装转移到一个抽象类或者接口中,比如一个“四方形”接口或者抽象类,然后让正方形和长方形分别实现四方形接口或者继承四方形抽象类,如下图所示
    在这里插入图片描述
  • 一般来说,只要有可能,就不要从具体类继承
  • 所有的继承都是从抽象类开始,而所有的具体类都没有子类。也就是说,在一个由继承关系形成的等级结构中,树叶节点都应当是具体类,树枝节点都应该是抽象类或者接口。

4.里氏替换原则的作用

1.里氏替换原则是实现开闭原则的重要方式之一。

2.它克服了继承中重写父类造成的可复用性变差的缺点。

3.它是动作正确性的保证。即类的扩展不会给已有的系统引入新的错误,降低了代码出错的可能性。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值