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