← 返回题目列表

模板方法模式如何防止子类破坏父类流程不变量?

高频 困难 第 14 / 26 题 更新于 2026/08/02
模板方法模式不变量继承风险防御式设计

简化版

模板方法模式依赖继承,最大风险是子类覆盖步骤时破坏父类不变量。防护手段包括:模板入口 final、关键步骤不可覆盖、抽象步骤契约写清楚、入参和返回值校验、限制可覆盖方法数量、使用钩子而不是任意覆盖、增加父类级测试和子类契约测试。若子类需要改动太多流程,说明不适合继续用模板方法。

详细版

父类的不变量指流程必须始终满足的规则,例如:

  • 导入前必须校验文件;
  • 保存前必须完成去重;
  • 资源打开后必须释放;
  • 任务结束必须记录审计日志;
  • 事务提交前不能发送外部通知。

模板方法通过父类控制流程来维护这些不变量。但子类如果能重写过多方法,就可能破坏这些规则。

public final void run() {
    lock();
    try {
        validate();
        doRun();
    } finally {
        unlock();
    }
}

这里 lock/unlock 不应该交给子类随便改,doRun 才是扩展点。

记忆钩子:模板方法最怕“父类定规矩,子类开后门”。

完整版教学

面试提示:子类破坏不变量时,重点不是责怪子类,而是检查模板是否把关键约束留给了可覆盖方法。

一、什么是不变量

不变量就是无论哪个子类执行,都必须成立的约束。

例如批处理任务:

获取锁 -> 校验参数 -> 执行业务 -> 记录结果 -> 释放锁

无论是导入用户、同步订单还是生成报表,都不能跳过释放锁。这就是父类要保护的不变量。

二、子类破坏不变量的常见方式

常见破坏方式包括:

破坏方式后果
重写模板入口跳过关键步骤
返回非法结果父类后续步骤异常
修改共享字段影响其他步骤
吞掉异常父类无法回滚或告警
调用外部副作用事务和一致性失控

继承让子类有很强能力,所以边界必须收紧。

三、模板入口 final 是第一道防线

public final JobResult execute(JobContext context) {
    check(context);
    before(context);
    JobResult result = doExecute(context);
    after(context, result);
    return result;
}

子类不能改整体顺序,只能实现 doExecute。这样父类至少能保证校验、前置、后置都执行。

四、关键步骤不要开放覆盖

并不是所有步骤都要 protected。关键流程可写成 private:

private void auditStart(JobContext context) {
    auditRepository.save(...);
}

如果子类不应该改变审计逻辑,就不要给它覆盖机会。扩展点越少,流程越稳。

五、抽象步骤要写清契约

抽象方法不是只写名字:

protected abstract List<Row> parse(File file);

还要约定:

  • 不能返回 null;
  • 不允许修改入参;
  • 解析失败要抛指定异常;
  • 返回 rows 必须已完成字段标准化;
  • 不允许在此步骤直接写数据库。

这些契约可以写在注释、接口文档和测试里。

六、父类要做防御式校验

不要完全相信子类:

List<Row> rows = parse(file);
if (rows == null) {
    throw new IllegalStateException("parse result must not be null");
}

也可以校验返回数量、状态、版本号、必填字段。父类校验能让错误更早暴露。

七、子类变化太大时应停止使用模板方法

如果某个子类需要跳过校验、改变顺序、替换事务边界、改异常策略,这说明它和父类流程不再是同一个模板。

此时可以:

  • 新建另一个模板父类;
  • 改成策略模式;
  • 使用组合编排;
  • 拆成多个小流程。

不要靠越来越多的钩子把父类变成条件迷宫。

八、常见误区与追问

  • 误区:子类越自由,模板方法越灵活。 子类过度自由会破坏父类流程不变量。
  • 误区:所有父类方法都 protected,方便扩展。 这会让关键步骤也暴露给子类破坏。
  • 误区:抽象方法不用写契约。 子类不知道返回值、异常和副作用边界,就很容易写错。
  • 误区:父类不需要校验子类结果。 父类应对关键返回值做防御式检查。
  • 追问:如何发现子类破坏流程? 看是否重写入口、跳过步骤、吞异常、改共享状态、做外部副作用。
  • 追问:钩子太多怎么办? 拆模板、改策略或组合,不要继续往父类塞条件。
  • 追问:模板方法如何测试不变量? 对父类写通用测试,对每个子类跑契约测试,验证关键步骤必执行。

九、加强记忆

  1. 不变量是所有子类都必须遵守的流程规则。
  2. 模板入口 final 防止子类改顺序。
  3. 关键步骤 private 或 final,不随便 protected。
  4. 抽象步骤要有清楚契约。
  5. 父类要校验子类返回值和异常边界。
  6. 子类差异太大时换模式,不硬塞模板。