跳到主要内容

模板方法模式-Template Method Pattern

序言​

泡一杯茶和冲一杯咖啡,步骤出奇地相似:

  1. 把水烧开;
  2. 冲泡(茶叶/咖啡粉);
  3. 把饮料倒进杯子;
  4. 加调料(茶加柠檬、咖啡加糖和牛奶)。

流程骨架相同,只有"冲泡"和"加调料"两步不同。如果为茶和咖啡各写一套完整流程,重复代码不可避免;而如果把"公共骨架"抽取到父类、把"差异步骤"留给子类实现——这就是模板方法模式(Template Method Pattern)。

模板方法模式的本质:父类定义一个算法的骨架(模板方法),把某些步骤延迟到子类实现。子类可以重新定义算法的某些步骤,但不能改变算法的整体结构。

定义​

Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm's structure.

定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重新定义该算法的某些特定步骤。

两个关键点:

  1. 骨架在父类:算法的整体流程(步骤顺序)由父类的模板方法固定;
  2. 步骤在子类:流程中可变的具体步骤由子类覆写实现。

结构​

让我们从一个问题开始​

假设我们要实现茶和咖啡两种饮料的制作。不用模板方法,两个类里会充满重复的"烧水、倒杯"代码:

public class Tea {

public void prepare() {
boilWater(); // 烧水
brewTea(); // 泡茶
pourInCup(); // 倒进杯子
addLemon(); // 加柠檬
}
}

public class Coffee {

public void prepare() {
boilWater(); // 烧水(重复代码)
brewCoffee(); // 冲咖啡
pourInCup(); // 倒进杯子(重复代码)
addSugarAndMilk(); // 加糖和牛奶
}
}

问题显而易见:烧水、倒杯的代码在两个类中重复;未来新增一种饮料(如热巧克力),又要复制一遍;如果"烧水"方式统一升级(比如改用温控壶),所有饮料类都要修改。

模板方法模式的结构​

把公共流程抽取到抽象父类 Beverage,差异步骤定义为抽象方法留给子类:

各个角色的职责如下:

  • AbstractClass(抽象类):定义模板方法(prepare)和算法骨架,声明抽象步骤(brew、addCondiments),提供公共步骤(boilWater、pourInCup),对应图中的 Beverage。
  • ConcreteClass(具体类):实现抽象步骤,覆写钩子方法,对应图中的 Tea、Coffee。
  • 钩子方法(Hook):抽象类中带默认实现的"可覆写"方法(如 isAddCondiments),子类可选覆写来影响算法流程。

关键设计:模板方法 prepare 在父类中定义了"做什么、按什么顺序做",子类只负责"怎么做"。整体结构不可变,细节实现可变。

代码实现​

我们来实现饮料制作流程。

定义抽象类(含模板方法)​

public abstract class Beverage {

// 模板方法:定义算法骨架,子类不能覆写整体流程
public final void prepare() {
boilWater();
brew();
pourInCup();
// 钩子方法:子类可以覆写,决定是否加调料
if (isAddCondiments()) {
addCondiments();
}
}

// 公共步骤:所有饮料相同,在父类实现
private void boilWater() {
System.out.println("把水烧开");
}

private void pourInCup() {
System.out.println("把饮料倒进杯子");
}

// 抽象步骤:冲泡方式由子类实现
protected abstract void brew();

// 抽象步骤:加什么调料由子类实现
protected abstract void addCondiments();

// 钩子方法:默认加调料,子类可覆写
protected boolean isAddCondiments() {
return true;
}
}

注意:模板方法 prepare 被声明为 final,防止子类篡改算法骨架;公共步骤声明为 private,子类不可见也不可改。

定义具体类​

public class Tea extends Beverage {

@Override
protected void brew() {
System.out.println("用沸水浸泡茶叶");
}

@Override
protected void addCondiments() {
System.out.println("加柠檬片");
}
}
public class Coffee extends Beverage {

@Override
protected void brew() {
System.out.println("用沸水冲泡咖啡粉");
}

@Override
protected void addCondiments() {
System.out.println("加糖和牛奶");
}

// 钩子方法示例:假设今天不想加糖,覆写返回 false
@Override
protected boolean isAddCondiments() {
return false;
}
}

客户端使用​

public class Main {

public static void main(String[] args) {
System.out.println("===== 制作茶 =====");
Beverage tea = new Tea();
tea.prepare();

System.out.println("===== 制作咖啡 =====");
Beverage coffee = new Coffee();
coffee.prepare();
}
}

输出结果:

===== 制作茶 =====
把水烧开
用沸水浸泡茶叶
把饮料倒进杯子
加柠檬片
===== 制作咖啡 =====
把水烧开
用沸水冲泡咖啡粉
把饮料倒进杯子

核心收益:

  1. 烧水、倒杯等公共逻辑只在父类实现一次,子类零重复;
  2. 算法骨架统一(先烧水 → 再冲泡 → 再倒杯),新饮料只需实现两个步骤;
  3. 钩子方法让子类可以"跳过"可选步骤,灵活控制流程。

在开源框架中的应用​

javax.servlet.http.HttpServlet:教科书级模板方法​

HttpServlet 是模板方法模式在 Java 生态中最经典的应用。从源码层面看,service() 是模板方法,doGet/doPost/doPut 等是延迟到子类的步骤,而 getLastModified 是一个钩子方法。

模板方法:service()​

// javax.servlet.http.HttpServlet#service(源码骨架)
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

String method = req.getMethod();

if (method.equals(METHOD_GET)) {
// GET 分支:默认带"最近修改时间"缓存协商逻辑
long lastModified = getLastModified(req); // 钩子:询问子类资源最后修改时间
if (lastModified == -1) {
// 子类没有覆写 getLastModified:直接执行 doGet
doGet(req, resp); // 步骤:延迟到子类
} else {
long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
if (ifModifiedSince < lastModified) {
maybeSetLastModified(resp, lastModified);
doGet(req, resp); // 资源已更新:正常响应
} else {
resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED); // 未修改:304
}
}
} else if (method.equals(METHOD_HEAD)) {
doHead(req, resp); // 步骤:延迟到子类
} else if (method.equals(METHOD_POST)) {
doPost(req, resp);
} else if (method.equals(METHOD_PUT)) {
doPut(req, resp);
} else if (method.equals(METHOD_DELETE)) {
doDelete(req, resp);
} else if (method.equals(METHOD_OPTIONS)) {
doOptions(req, resp);
} else if (method.equals(METHOD_TRACE)) {
doTrace(req, resp);
} else {
// 未实现的方法:返回 405
resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
}
}

钩子方法的默认行为:doGet()​

doGet 在父类中的默认实现是返回 405 错误——子类如果不覆写,就等于"不支持 GET 请求":

// javax.servlet.http.HttpServlet#doGet(源码骨架:钩子的默认行为)
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String msg = lStrings.getString("http.method_get_not_supported");
resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, msg); // 405
}

钩子方法:getLastModified()​

// javax.servlet.http.HttpServlet#getLastModified(源码骨架:钩子的默认实现)
protected long getLastModified(HttpServletRequest req) {
return -1; // 默认:不支持缓存协商;子类可覆写返回资源的最后修改时间
}

完整的分工:

  • 骨架(父类固定):读取 HTTP 方法 → 按方法分发 → 缓存协商 → 405 兜底,这一切由 service() 固定;
  • 步骤(子类实现):doGet/doPost/doPut 等,由具体 Servlet 覆写;
  • 钩子(子类可选覆写):getLastModified 默认返回 -1(关闭缓存协商),覆写后自动启用 304 缓存——钩子不改变骨架,但可以影响流程分支。

这就是"框架定骨架,应用填步骤"的标准模板方法分工。

Spring 的 JdbcTemplate:execute → doExecute 骨架​

Spring 的 JdbcTemplate 把"获取连接 → 执行 SQL → 处理结果 → 释放资源"固定成骨架,把"具体执行什么 SQL、如何映射结果"留给回调(步骤)。从源码层面看:

// org.springframework.jdbc.core.JdbcTemplate#execute(源码骨架)
public <T> T execute(StatementCallback<T> action) throws DataAccessException {
Assert.notNull(action, "Callback object must not be null");

// 1. 骨架步骤:获取连接(从数据源或事务同步器)
Connection con = DataSourceUtils.getConnection(obtainDataSource());
Statement stmt = null;
try {
stmt = con.createStatement();
// 2. 模板步骤:设置语句属性(超时、fetchSize)——protected 方法,子类可覆写
applyStatementSettings(stmt);
// 3. 调用方步骤:执行语句并映射结果(回调)
T result = action.doInStatement(stmt);
// 4. 模板步骤:处理 SQL 警告
handleWarnings(stmt);
return result;
} catch (SQLException ex) {
// 5. 骨架步骤:统一异常转换(SQLException -> DataAccessException)
throw translateException("StatementCallback", sql, ex);
} finally {
// 6. 骨架步骤:关闭语句、释放连接
JdbcUtils.closeStatement(stmt);
DataSourceUtils.releaseConnection(con, obtainDataSource());
}
}

可覆写的模板步骤​

applyStatementSettings、handleWarnings 都是 protected 方法——它们是模板方法中"可以被子类调整的步骤":

// org.springframework.jdbc.core.JdbcTemplate(源码骨架)
// 设置语句超时/抓取大小:JdbcTemplate 的默认实现
protected void applyStatementSettings(Statement stmt) throws SQLException {
int fetchSize = getFetchSize();
if (fetchSize != -1) {
stmt.setFetchSize(fetchSize);
}
int maxRows = getMaxRows();
if (maxRows != -1) {
stmt.setMaxRows(maxRows);
}
...
}

调用方步骤:回调接口​

StatementCallback 是"延迟到调用方的步骤":

// org.springframework.jdbc.core.StatementCallback(源码)
@FunctionalInterface
public interface StatementCallback<T> {
// 调用方实现:拿到 Statement 后执行自己的 SQL 并返回结果
T doInStatement(Statement stmt) throws SQLException, DataAccessException;
}

query(...)、update(...) 等方法内部都是 execute(callback) 的不同回调封装。连接获取/释放、异常转换、资源管理这些骨架逻辑在 JdbcTemplate 中只写了一份,无论执行什么 SQL 都复用——这正是模板方法模式"复用骨架、延迟步骤"的价值。RestTemplate、RedisTemplate 等 Spring 家族组件同理。

适用场景​

  • 当多个算法共享相同的流程骨架,仅部分步骤不同时,模板方法可以消除重复代码、统一流程。
  • 当框架需要提供"定骨架、填步骤"的扩展机制时(如 Servlet、模板引擎、测试框架),模板方法是标准答案。
  • 当希望控制子类的扩展点(哪些步骤可以覆写、哪些必须覆写、哪些是可选钩子)时,模板方法可以精确限定变化范围。
  • 注意:模板方法依赖继承,继承层次过深会难以维护;如果算法骨架也需要变化,应该考虑策略模式或组合方式。

与其他设计模式的关系​

  • 模板方法模式与策略模式(Strategy)都封装算法,但手段不同:模板方法用继承固定骨架、子类覆写步骤;策略用组合替换整个算法。当"骨架不变、步骤可变"时选模板方法,当"算法整体可替换"时选策略。
  • 模板方法的抽象步骤由子类创建对象时,就变成了工厂方法模式(Factory Method)——工厂方法本质上是模板方法在"对象创建"步骤上的特化。
  • 模板方法内部步骤的执行顺序固定,如果需要动态调整顺序,可以结合责任链模式(Chain of Responsibility)。

总结​

模板方法模式用继承关系实现"复用骨架、延迟细节":

  • 算法骨架(模板方法)在父类中定义并固定,公共步骤在父类实现;
  • 差异步骤由子类实现,钩子方法提供可选流程控制;
  • 框架与业务的分工清晰:框架定骨架,业务填步骤。

模板方法模式的缺点是:继承带来的强耦合(子类无法脱离父类结构)和覆写权限的控制难度。但在流程固定、步骤可变的场景(Servlet、JdbcTemplate、各类框架扩展点)中,它是不可替代的基础模式。