什么是模板方法模式?应用场景有哪些?


面试考察点

  1. 设计思想理解:面试官不仅仅是想知道 "父类定义骨架,子类实现细节" 这句话,更是想看你是否理解这种 "固定算法骨架,开放扩展点" 的设计哲学,以及它如何体现 "好莱坞原则"(Don't call us, we'll call you)。

  2. 钩子方法认知:能否区分模板方法中的 "基本方法" 和 "钩子方法",以及钩子方法在实际框架中如何提供灵活的扩展能力。

  3. 源码关联能力:能否把模板方法模式和 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() 由子类实现BaseExecutorSimpleExecutor
数据导入导出定义 "校验 → 处理 → 输出" 骨架,具体格式由子类定Excel/PDF/CSV 导出
批量任务处理定义 "初始化 → 分片 → 执行 → 汇总" 骨架定时任务框架

面试高频追问

  1. 模板方法模式和策略模式有什么区别?

    • 模板方法通过继承实现,在父类固定算法骨架;策略模式通过组合实现,算法之间完全独立可以互换。模板方法强调 "算法骨架不变,步骤可变",策略模式强调 "整个算法可替换"。
  2. 钩子方法有什么用?能不能不用?

    • 钩子方法提供可选的扩展点,让子类在不改变算法骨架的前提下微调行为。不用钩子方法也能工作,但灵活性会差很多。Spring 的 refresh() 里如果没有钩子方法,各种子容器就没法做差异化扩展了。
  3. 模板方法为什么要用 final 修饰?

    • 防止子类重写模板方法,保证算法骨架不被破坏。这是模板方法模式的底线——骨架不可变,细节可扩展。当然,final 不是强制的,但最佳实践建议加上。

常见面试变体

  • "模板方法模式和策略模式的区别?"
  • "Spring 的 refresh() 方法用到了什么设计模式?"
  • "HttpServletservice() 方法体现了什么设计思想?"
  • "钩子方法是什么?有什么用?"

记忆口诀

核心思想:父类定骨架,子类填细节;不变封起来,变化留出去。

四种方法:模板定流程(final)、抽象必须写、具体大家用、钩子看着办。

vs 策略模式:模板靠继承改步骤,策略靠组合换算法。

总结

模板方法模式的精髓就是 "固定算法骨架,开放扩展点"。面试时先说概念,手写一个简单实现,然后重点讲 HttpServlet 和 Spring refresh() 这两个经典源码案例,最后和策略模式做区分。能把框架源码里的模板方法讲清楚,面试官就知道你不只是背了概念,而是真的读过源码。