用契约视角核对子类
里氏替换原则说的是:用基类引用指向子类时,程序行为不应被破坏。落到工程上就是一次契约审查——先列出基类对外承诺的可见行为,再逐条对照子类实现。契约不只包含入参与返回值,还包括异常语义、副作用与调用顺序。拿不准时最有效的办法是写一套基类测试,参数化地跑遍所有子类。
基本信息
| 审查对象 | 前置条件 / 后置条件 / 不变量 / 副作用 |
|---|---|
| 手段 | 基类测试套件参数化跑全部子类 |
| 结论 | 无法满足契约 → 改用组合而不是继承 |
| 语言差异 | 协变返回值通常允许,参数类型收紧通常不允许 |
常见违约形态
- 子类收紧了入参范围:基类接受任意正数,子类只接受偶数
- 子类放宽了后置条件:基类保证返回非空,子类可能返回空集合
- 子类抛出基类未声明的新异常类型,调用方的错误处理失配
- 子类产生额外副作用:基类只读,子类顺手写了审计日志或缓存
- 子类覆盖方法里直接抛出不支持操作,调用方在运行时才失败
- 子类破坏不变量:基类保证余额非负,子类允许多扣一次
参数化契约测试
参数化契约测试
abstract class ShapeTest {
abstract Shape create(); // 子类测试各自提供实例
@Test
void areaIsPositive() {
assertTrue(create().area().isPositive());
}
@Test
void areaIsStable() { // 不变量:重复调用结果一致
Shape s = create();
assertEquals(s.area(), s.area());
}
}
class SquareTest extends ShapeTest {
@Override Shape create() { return new Square(2); }
}契约四要素对照
| 要素 | 含义 | 子类的允诺方向 |
|---|---|---|
| 前置条件 | 调用方必须满足 | 只能放宽,不能收紧 |
| 后置条件 | 返回时必须成立 | 只能加强,不能放宽 |
| 不变量 | 对象生命期内恒真 | 必须保持 |
| 副作用 | 对外可见的额外影响 | 不得超出基类声明 |
继承关系里最容易出问题的不是方法签名,而是历史约束:基类方法隐含「只能调用一次」这类顺序要求时,子类很难在签名上体现出来。这类约束只能靠契约测试与文档共同表达。
把契约测试组织成抽象基类测试、由每个子类的测试类继承,是 Java 生态里常用的写法。它让替换安全性有了统一证据,也让新增子类时不会漏掉关键断言。