模板方法为什么通常要声明为 final?步骤方法访问权限怎么设计?
简化版
模板方法通常声明为 final,目的是保护固定流程骨架,防止子类重写整个流程、跳过关键步骤或改变执行顺序。可变步骤一般设计成 protected abstract 或 protected 钩子方法,供子类在受控范围内扩展;公共入口保持稳定,内部步骤不随意暴露成 public。核心原则是:父类控制流程,子类只填变化点。
详细版
模板方法模式的价值在于“流程固定,步骤可变”。如果模板方法本身可以被子类随意重写,那么固定流程就失去保护:
abstract class ImportTemplate {
public final void importFile(File file) {
validate(file);
List<Row> rows = parse(file);
save(rows);
afterImport(rows);
}
protected abstract List<Row> parse(File file);
protected void afterImport(List<Row> rows) {}
}
这里 importFile 是稳定入口,声明为 final;parse 是必须由子类实现的变化步骤;afterImport 是可选钩子。
如果把所有方法都设成 public 或模板入口不加限制,子类和外部调用方都可能绕过父类流程,导致校验、事务、日志、资源释放等关键逻辑失效。
记忆钩子:模板方法的
final不是炫技,而是在说“流程骨架归父类管,子类别改剧本”。
完整版教学
面试提示:访问控制题要讲清楚哪些方法允许扩展、哪些方法必须封死,这是模板方法保证流程不变形的关键。
一、模板方法模式的核心是保护流程骨架
模板方法模式把公共流程沉淀在父类中:
step1 校验
step2 解析
step3 保存
step4 后置处理
不同子类只改其中少数步骤。父类要保证所有子类都按同一个顺序执行,否则模板方法模式就退化成普通继承。
二、为什么模板方法常用 final
如果模板入口可重写:
class BadImporter extends ImportTemplate {
public void importFile(File file) {
save(parse(file)); // 跳过 validate
}
}
子类可以跳过校验、审计、锁、事务、资源释放等关键步骤。final 的价值就是防止这种破坏。
public final void importFile(File file) {
validate(file);
List<Row> rows = parse(file);
save(rows);
}
这让父类的流程契约稳定下来。
三、步骤方法通常用 protected
步骤方法给子类实现,不一定给外部调用方使用,所以常用 protected:
protected abstract List<Row> parse(File file);
protected void afterImport(List<Row> rows) {}
这样外部只能调用模板入口 importFile,不能直接调用 parse 绕过流程。
四、抽象方法和钩子方法要区分
| 类型 | 写法 | 含义 |
|---|---|---|
| 必填步骤 | protected abstract | 子类必须实现 |
| 默认步骤 | protected 默认实现 | 子类可覆盖 |
| 判断钩子 | protected boolean | 控制可选分支 |
| 固定步骤 | private 或 final | 子类不能改 |
不是所有步骤都要交给子类。越关键、越稳定的步骤,越应该留在父类。
五、不要把步骤方法都做成 public
如果步骤方法是 public:
importer.parse(file);
importer.save(rows);
外部调用方可能绕开模板流程,直接调内部步骤。这样父类提供的校验、异常处理、事务边界都可能失效。
模板方法模式一般只暴露一个或少数几个公共入口。
六、final 不是所有语言和框架都必须
有些语言没有 final,有些框架为了代理或测试不建议滥用 final。这时要通过约定、文档、测试或访问控制保护流程。
面试回答可以说:Java 中常见做法是模板方法声明为 final;如果受框架限制不能 final,也要明确“不允许子类重写模板入口”的契约。
七、访问权限体现扩展边界
一个稳的模板类大致是:
public abstract class AbstractJob {
public final void execute() {
before();
doExecute();
after();
}
private void before() {}
protected abstract void doExecute();
protected void after() {}
}
public 是对外入口,private 是父类内部固定逻辑,protected 是留给子类的变化点。
八、常见误区与追问
- 误区:模板方法不加
final也完全一样。 子类可重写入口后,父类就无法保证流程顺序和关键步骤。 - 误区:步骤方法都应该 public,方便调用。 这会让外部绕开模板流程,破坏封装。
- 误区:所有 protected 方法都应该让子类覆盖。 稳定步骤可以 private 或 final,避免扩展点泛滥。
- 误区:用了 final 就一定是模板方法。 final 只是保护手段,核心仍是固定骨架和可变步骤。
- 追问:Spring 代理遇到 final 方法怎么办? CGLIB 代理不能覆盖 final 方法,需结合具体框架机制选择设计。
- 追问:步骤方法用 protected 还是 private? 子类需要扩展用 protected;父类固定逻辑用 private 或 final。
- 追问:钩子方法是否应该 abstract? 可选扩展通常给默认实现;必须由子类提供的步骤才 abstract。
九、加强记忆
- 模板入口常用
final保护流程骨架。 - 子类只实现变化步骤,不改整体剧本。
- 必填步骤用
protected abstract。 - 可选扩展用 protected 钩子默认实现。
- 固定步骤用 private 或 final 收紧边界。
- 对外只暴露模板入口,避免绕过流程。