首页/开发/代码质量与评审速查/SOLID 落地:用契约视角核对子类
设计原则与模式62 条

SOLID 落地:用契约视角核对子类

给子类做契约审查:列举基类的可见承诺(入参范围、返回值范围、异常、副作用),逐条对照子类实现;拿不准的用基类引用编写集成测试,统一套跑所有子类。

用契约视角核对子类

里氏替换原则说的是:用基类引用指向子类时,程序行为不应被破坏。落到工程上就是一次契约审查——先列出基类对外承诺的可见行为,再逐条对照子类实现。契约不只包含入参与返回值,还包括异常语义、副作用与调用顺序。拿不准时最有效的办法是写一套基类测试,参数化地跑遍所有子类。

基本信息

审查对象前置条件 / 后置条件 / 不变量 / 副作用
手段基类测试套件参数化跑全部子类
结论无法满足契约 → 改用组合而不是继承
语言差异协变返回值通常允许,参数类型收紧通常不允许

常见违约形态

  • 子类收紧了入参范围:基类接受任意正数,子类只接受偶数
  • 子类放宽了后置条件:基类保证返回非空,子类可能返回空集合
  • 子类抛出基类未声明的新异常类型,调用方的错误处理失配
  • 子类产生额外副作用:基类只读,子类顺手写了审计日志或缓存
  • 子类覆盖方法里直接抛出不支持操作,调用方在运行时才失败
  • 子类破坏不变量:基类保证余额非负,子类允许多扣一次

参数化契约测试

参数化契约测试
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 生态里常用的写法。它让替换安全性有了统一证据,也让新增子类时不会漏掉关键断言。

易错点

  • 契约只靠注释:无测试兜底,子类悄悄违约也无人发现
  • 子类测试各写各的:替换安全性没有统一证据,接口设计问题反复出现
  • 用继承复用代码而非表达「是一个」:子类被迫实现不需要的方法,只能抛出不支持操作
  • 发现违约后继续用继承加判断:在基类里按子类类型分支,抽象彻底失效

数据来源与参考

  • Liskov & Wing(1994)行为子类型理论与工程化契约测试实践
​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌