模板方法模式-Template Method Pattern
序言
泡一杯茶和冲一杯咖啡,步骤出奇地相似:
- 把水烧开;
- 冲泡(茶叶/咖啡粉);
- 把饮料倒进杯子;
- 加调料(茶加柠檬、咖啡加糖和牛奶)。
流程骨架相同,只有"冲泡"和"加调料"两步不同。如果为茶和咖啡各写一套完整流程,重复代码不可避免;而如果把"公共骨架"抽取到父类、把"差异步骤"留给子类实现——这就是模板方法模式(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.
定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重新定义该算法的某些特定步骤。
两个关键点:
- 骨架在父类:算法的整体流程(步骤顺序)由父类的模板方法固定;
- 步骤在子类:流程中可变的具体步骤由子类覆写实现。
结构
让我们从一个问题开始
假设我们要实现茶和咖啡两种饮料的制作。不用模板方法,两个类里会充满重复的"烧水、倒杯"代码:
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();
}
}
输出结果:
===== 制作茶 =====
把水烧开
用沸水浸泡茶叶
把饮料倒进杯子
加柠檬片
===== 制作咖啡 =====
把水烧开
用沸水冲泡咖啡粉
把饮料倒进杯子
核心收益:
- 烧水、倒杯等公共逻辑只在父类实现一次,子类零重复;
- 算法骨架统一(先烧水 → 再冲泡 → 再倒杯),新饮料只需实现两个步骤;
- 钩子方法让子类可以"跳过"可选步骤,灵活控制流程。
在开源框架中的应用
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);
}
...
}