模板方法模式如何防止子类破坏父类流程不变量?
简化版
模板方法模式依赖继承,最大风险是子类覆盖步骤时破坏父类不变量。防护手段包括:模板入口 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,方便扩展。 这会让关键步骤也暴露给子类破坏。
- 误区:抽象方法不用写契约。 子类不知道返回值、异常和副作用边界,就很容易写错。
- 误区:父类不需要校验子类结果。 父类应对关键返回值做防御式检查。
- 追问:如何发现子类破坏流程? 看是否重写入口、跳过步骤、吞异常、改共享状态、做外部副作用。
- 追问:钩子太多怎么办? 拆模板、改策略或组合,不要继续往父类塞条件。
- 追问:模板方法如何测试不变量? 对父类写通用测试,对每个子类跑契约测试,验证关键步骤必执行。
九、加强记忆
- 不变量是所有子类都必须遵守的流程规则。
- 模板入口 final 防止子类改顺序。
- 关键步骤 private 或 final,不随便 protected。
- 抽象步骤要有清楚契约。
- 父类要校验子类返回值和异常边界。
- 子类差异太大时换模式,不硬塞模板。