什么是模板方法模式?应用场景有哪些?
面试考察点
-
设计思想理解:面试官不仅仅是想知道 "父类定义骨架,子类实现细节" 这句话,更是想看你是否理解这种 "固定算法骨架,开放扩展点" 的设计哲学,以及它如何体现 "好莱坞原则"(Don't call us, we'll call you)。
-
钩子方法认知:能否区分模板方法中的 "基本方法" 和 "钩子方法",以及钩子方法在实际框架中如何提供灵活的扩展能力。
-
源码关联能力:能否把模板方法模式和 JDK、Spring、MyBatis 等框架的源码联系起来。面试官最爱听的就是 "我看过
HttpServlet的源码,里面用的就是模板方法"。
核心答案
一句话定义:模板方法模式在父类中定义一个算法的骨架(模板方法),将某些步骤延迟到子类中实现。子类可以在不改变算法整体结构的前提下,重写算法的特定步骤。
打个比方:做菜。不管做什么菜,流程都差不多——备料 → 热锅 → 炒菜 → 装盘。这个流程是固定的(模板方法),但每道菜的备料内容、炒菜方式各不相同(子类实现)。你不需要重新发明 "做菜流程",只需要在固定步骤里填入自己的实现。
上面的图展示了模板方法模式的核心结构:
- 模板方法(
templateMethod):定义算法骨架,通常用final修饰防止子类重写。它按固定顺序调用各个步骤方法。 - 抽象方法(
abstractStep):子类必须实现的步骤,属于算法中变化的部分。 - 具体方法:父类已经实现好的固定逻辑,所有子类共用。
- 钩子方法(
hook):父类提供默认实现(通常是空方法),子类可选重写,用于微调算法行为。
核心思想就是:不变的部分封在父类,变化的部分留给子类。这是 "开闭原则" 的经典体现——对扩展开放(新增子类),对修改关闭(不改模板方法)。
深度解析
一、手写模板方法模式
用 "数据库查询" 这个经典场景来写一个完整示例。
// 抽象父类:定义查询流程骨架
public abstract class DataQueryTemplate {
// 模板方法:定义算法骨架,用 final 防止子类覆盖
public final void execute() {
getConnection(); // ① 获取连接
String sql = buildSql(); // ② 构建SQL(子类实现)
System.out.println("执行 SQL:" + sql);
processResult(); // ③ 处理结果(子类实现)
if (isLogEnabled()) { // ④ 钩子方法:是否记录日志
System.out.println("[日志] 查询执行完毕");
}
closeConnection(); // ⑤ 关闭连接
}
// 固定步骤:所有子类共用
private void getConnection() {
System.out.println("获取数据库连接");
}
private void closeConnection() {
System.out.println("关闭数据库连接");
}
// 抽象方法:子类必须实现
protected abstract String buildSql();
protected abstract void processResult();
// 钩子方法:子类可选重写(默认不记录日志)
protected boolean isLogEnabled() {
return false;
}
}
两个具体子类:
// 查询用户
public class UserQuery extends DataQueryTemplate {
@Override
protected String buildSql() {
return "SELECT * FROM user WHERE age > 18";
}
@Override
protected void processResult() {
System.out.println("将用户数据映射为 User 对象列表");
}
@Override
protected boolean isLogEnabled() {
return true; // 用户查询开启日志
}
}
// 查询订单
public class OrderQuery extends DataQueryTemplate {
@Override
protected String buildSql() {
return "SELECT * FROM order WHERE status = 'UNPAID'";
}
@Override
protected void processResult() {
System.out.println("将订单数据映射为 Order 对象列表");
}
// 不重写 hook,使用默认值(不记录日志)
}
使用:
DataQueryTemplate userQuery = new UserQuery();
userQuery.execute();
System.out.println("----------");
DataQueryTemplate orderQuery = new OrderQuery();
orderQuery.execute();
输出:
获取数据库连接
执行 SQL:SELECT * FROM user WHERE age > 18
将用户数据映射为 User 对象列表
[日志] 查询执行完毕
关闭数据库连接
----------
获取数据库连接
执行 SQL:SELECT * FROM order WHERE status = 'UNPAID'
将订单数据映射为 Order 对象列表
关闭数据库连接
看到没?execute() 的执行流程完全一样(获取连接 → 构建 SQL → 处理结果 → 关闭连接),但 SQL 怎么构建、结果怎么处理,由子类决定。钩子方法 isLogEnabled() 提供了额外的灵活度——UserQuery 开启了日志,OrderQuery 用默认值不记录。新增查询场景只需要写一个子类,完全不用改父类。
二、模板方法中的四种方法
| 方法类型 | 修饰方式 | 实现位置 | 是否必须重写 | 作用 |
|---|---|---|---|---|
| 模板方法 | final | 父类 | ❌ 不允许 | 定义算法骨架 |
| 抽象方法 | abstract | 子类 | ✅ 必须 | 实现变化的步骤 |
| 具体方法 | 普通 | 父类 | ❌ 可选 | 提供公共逻辑 |
| 钩子方法 | 普通(空实现) | 父类默认,子类可选 | ❌ 可选 | 提供扩展点 |
钩子方法是模板方法模式里一个很妙的设计。它让子类可以在不改变算法骨架的前提下,对流程进行微调。比如上面的 isLogEnabled() 控制是否记录日志,就是一个典型的钩子。
三、JDK 中的经典应用:HttpServlet
这是模板方法模式在 JDK 中最经典的应用,面试必提。
// HttpServlet 的模板方法
public abstract class HttpServlet extends GenericServlet {
// 这是模板方法!根据请求类型分发到具体的 doXxx 方法
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) {
String method = req.getMethod();
if ("GET".equals(method)) {
doGet(req, resp);
} else if ("POST".equals(method)) {
doPost(req, resp);
} else if ("PUT".equals(method)) {
doPut(req, resp);
}
// ... 其他请求类型
}
// 这些就是 "抽象方法"(虽然不是 abstract,但默认返回 405 错误)
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
resp.sendError(405, "Method Not Allowed");
}
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
resp.sendError(405, "Method Not Allowed");
}
}
你自己写 Servlet 的时候,只需要继承 HttpServlet,重写 doGet() 和 doPost():
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
// 你只关心自己的业务逻辑
resp.getWriter().write("Hello World");
}
}
service() 方法就是模板方法——它定义了请求处理的骨架(获取请求类型 → 分发到对应方法)。你不需要关心这个分发逻辑,只需要在 doGet() 里写业务代码就行。
四、Spring 中的经典应用:AbstractApplicationContext
Spring 容器的刷新流程 refresh() 方法也是一个典型的模板方法。
// AbstractApplicationContext#refresh()(简化版)
public void refresh() {
prepareRefresh(); // ① 准备工作
obtainFreshBeanFactory(); // ② 创建 BeanFactory
prepareBeanFactory(beanFactory); // ③ 配置 BeanFactory
postProcessBeanFactory(beanFactory); // ④ 钩子方法!子类可扩展
invokeBeanFactoryPostProcessors(); // ⑤ 执行 BeanFactory 后置处理器
registerBeanPostProcessors(); // ⑥ 注册 Bean 后置处理器
initMessageSource(); // ⑦ 初始化消息源
initApplicationEventMulticaster(); // ⑧ 初始化事件广播器
onRefresh(); // ⑨ 钩子方法!子类可扩展
registerListeners(); // ⑩ 注册监听器
finishBeanFactoryInitialization(); // ⑪ 完成 Bean 初始化
finishRefresh(); // ⑫ 完成刷新
}
其中 postProcessBeanFactory() 和 onRefresh() 就是钩子方法,留给子类扩展。比如 AnnotationConfigServletWebServerApplicationContext 就重写了 onRefresh() 来创建内嵌的 Web 容器。
这个方法的骨架从 Spring 诞生到现在就没大改过,但通过钩子方法,它支撑了无数种不同场景的扩展。
五、实际应用场景
| 场景 | 模板方法角色 | 举例 |
|---|---|---|
| Servlet 请求处理 | service() 是模板,doGet()/doPost() 是扩展点 | HttpServlet |
| Spring 容器刷新 | refresh() 是模板,onRefresh() 等是钩子 | AbstractApplicationContext |
| MyBatis 执行器 | query() 定义骨架,doQuery() 由子类实现 | BaseExecutor → SimpleExecutor |
| 数据导入导出 | 定义 "校验 → 处理 → 输出" 骨架,具体格式由子类定 | Excel/PDF/CSV 导出 |
| 批量任务处理 | 定义 "初始化 → 分片 → 执行 → 汇总" 骨架 | 定时任务框架 |
面试高频追问
-
模板方法模式和策略模式有什么区别?
- 模板方法通过继承实现,在父类固定算法骨架;策略模式通过组合实现,算法之间完全独立可以互换。模板方法强调 "算法骨架不变,步骤可变",策略模式强调 "整个算法可替换"。
-
钩子方法有什么用?能不能不用?
- 钩子方法提供可选的扩展点,让子类在不改变算法骨架的前提下微调行为。不用钩子方法也能工作,但灵活性会差很多。Spring 的
refresh()里如果没有钩子方法,各种子容器就没法做差异化扩展了。
- 钩子方法提供可选的扩展点,让子类在不改变算法骨架的前提下微调行为。不用钩子方法也能工作,但灵活性会差很多。Spring 的
-
模板方法为什么要用
final修饰?- 防止子类重写模板方法,保证算法骨架不被破坏。这是模板方法模式的底线——骨架不可变,细节可扩展。当然,
final不是强制的,但最佳实践建议加上。
- 防止子类重写模板方法,保证算法骨架不被破坏。这是模板方法模式的底线——骨架不可变,细节可扩展。当然,
常见面试变体
- "模板方法模式和策略模式的区别?"
- "Spring 的
refresh()方法用到了什么设计模式?" - "
HttpServlet的service()方法体现了什么设计思想?" - "钩子方法是什么?有什么用?"
记忆口诀
核心思想:父类定骨架,子类填细节;不变封起来,变化留出去。
四种方法:模板定流程(final)、抽象必须写、具体大家用、钩子看着办。
vs 策略模式:模板靠继承改步骤,策略靠组合换算法。
总结
模板方法模式的精髓就是 "固定算法骨架,开放扩展点"。面试时先说概念,手写一个简单实现,然后重点讲 HttpServlet 和 Spring refresh() 这两个经典源码案例,最后和策略模式做区分。能把框架源码里的模板方法讲清楚,面试官就知道你不只是背了概念,而是真的读过源码。
