定义
任何基类可以出现的地方,子类一定可以出现。
里氏替换原则是继承复用的基石,只有当子类可以替换掉基类且软件单位的功能不受到影响的时候,基类才能真正被复用,而子类也能够在基类的基础上去增加新的行为。
里氏替换原则通俗点来说就是:子类可以扩展父类的功能,但不能改变父类原有的功能。
通过以上这句话,可以引申出以下几点含义:
- 含义一:子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
- 含义二:子类中可以增加自己特有的方法。
- 含义三:当子类的方法重载父类的方法时,方法的前置条件(即方法的输入、入参)要比父类方法的输入参数更宽松。
- 含义四:当子类的方法实现父类的方法时(重写、重载或者实现抽象方法),方法的后置条件(即方法的输出、返回值)要比父类更严格或相等。
如果程序违背了里氏替换原则,则继承类的对象在基类出现的地方会出现运行错误。这时其修正方法是:取消原来的继承关系,重新设计它们之间的关系(可以采用依赖、聚集、组合等关系)。
含义讲解
在讲解开闭原则时,笔者通过扩展子类来覆写父类方法,同时子类提供额外方法来实现需求改动。👉点击跳转
代码粘贴至此:
1 | /** |
含义一讲解
子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
上段代码中 getPrice()
其实已经对于原来的 Food
类中的 getPrice()
含义发生了变化,对其进行了打折。但这并不符合里氏替换原则。所以这里我们将打折的方法放置到新增的方法中,调整后的代码如下:
1 | /** |
这样调整后,我们可以看到子类并没有覆盖父类的非抽象方法 getPrice()
。
含义二讲解
子类中可以增加自己特有的方法。
含义一讲解的代码片段中 getDiscountPrice()
是子类新增的特有方法,其父类中并不存在该方法。
含义三讲解
当子类的方法重载父类的方法时,方法的前置条件(即方法的输入/入参)要比父类方法的输入参数更宽松。
1 | /** |
测试类的输出结果如下:
父类被执行
这个输出结构是正确的。
父类的方法入参是 HashMap
类型,而子类的方法入参是 Map
类型,子类的方法入参类型范围比父类大,那么子类的方法永远也不会被执行。
那我们反过来试下,调整代码如下。
1 | public class Father { |
测试类的输出结果如下:
子类HashMap被执行
可以看到这个时候程序执行了子类的 method
方法,这样就违反了里氏替换原则,在实际的开发过程当中很容易引起业务逻辑的混乱。
含义四讲解
当子类的方法实现父类的方法时(重写、重载或者实现抽象方法),方法的后置条件(即方法的输出、返回值)要比父类更严格或相等。
1 | /** |
测试类的输出结果如下:
子类method方法被执行
{username=zhaohaihao}
也就是说在实现父类的抽象方法时,子类的方法返回值范围一定要小于父类方法的返回值范围。
那我们反过来试下,我们将子类方法的返回值改为 Object
,而父类方法的返回值仍保持 Map
不变。
Object
是所有类的基类,因此它的范围是大于 Map
的,这个时候编辑器已经给出了错误提示,如上图。
继承的缺点
- 继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法
- 降低了代码的灵活性
- 增强了耦合性。当父类的常量、变量或者方法被修改时,必需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果——大片的代码需要重构
优点
- 约束继承泛滥(开闭原则的一种体现)
- 加强程序健壮性,在程序变更的同时可以做到良好的兼容性
- 提高代码的重用性
- 提高代码的可扩展性
里氏替换原则反应了基类与子类之间的关系,同时也是对开闭原则的补充以及对实现抽象化的具体步骤的规范。
参考
- 《Head First 设计模式》
- 《大话设计模式》
- 设计模式之里氏替换原则