里氏替换原则是什么
里氏替换原则(LSP)要求子类型能够替换父类型而不破坏调用方所依赖的行为契约:子类可以放宽前置条件、可以加强后置保证,但不能反过来——不能对入参提出更严格的要求,也不能削弱对结果的承诺,更不能不保持父类的不变量。判断是否违反有个实用方法:看调用方是否需要做类型判断或写特殊分支才能正确工作。典型反例是让正方形继承矩形,看起来符合直觉,实际破坏了调用方对宽高独立变化的假设。
基本信息
| 契约规则 | 子类可放宽前置条件、可加强后置保证,不可反向 |
|---|---|
| 不变量 | 父类承诺始终成立的约束,子类必须保持 |
| 异常约定 | 子类不应抛出父类未声明的异常 |
| 识别方法 | 调用方是否需要 instanceof 判断或特殊分支才能正确工作 |
| 修复方向 | 改为组合、不可变值对象或重新设计继承层级 |
典型使用场景
- 继承层级评审:检查子类是否改变了父类的语义而不是只扩展行为
- 接口实现约定:实现类不能悄悄收窄参数范围或加强校验
- 多态调用链路:替换实现后调用方逻辑是否需要特判
- 测试替身设计:桩对象的行为要符合真实实现的契约,否则测试失真
- 框架扩展点:插件实现同一接口时,行为契约必须写在文档里
- 重构决策:判断某处继承关系是否应改为组合
契约与违反表现
| 契约项 | 含义 | 违反时的表现 |
|---|---|---|
| 前置条件 | 调用方必须满足的输入约束 | 子类要求更多输入,旧调用方报错 |
| 后置条件 | 方法返回后必须成立的保证 | 子类返回弱化结果或部分完成 |
| 不变量 | 对象生命周期内恒成立的约束 | 子类破坏字段间关系,状态不一致 |
| 异常约定 | 允许抛出的异常集合 | 子类抛出新异常,调用方处理逻辑失效 |
| 副作用约定 | 对外部状态与调用顺序的影响 | 子类悄悄改变了状态变更顺序 |
违反与修复示例
违反与修复示例
# 违反:子类改变父类语义
class 矩形 { setW(w); setH(h) } # 宽高可独立变化
class 正方形 extends 矩形 { setW 同时改高 } # 调用方假设被破坏
# 现象:调用方连续设置宽高后,读到的尺寸与预期不符
# 修复方向一:取消继承,改为不可变值对象与工厂方法
# 修复方向二:抽出更小的接口(如面积计算),分别实现,不做父子关系
# 检验:调用方不再需要 instanceof 或特殊分支即可正确工作行为契约比方法签名更重要:签名兼容但语义不同(例如子类把空值处理改为抛异常)同样违反 LSP,这类问题往往在替换实现后才暴露。
继承表达的是「是一种、且行为可替换」;如果只是复用代码,组合通常更安全,也更不容易破坏调用方的既有假设。