模板方法模式是 GoF 23 个模式里"最像继承本身"的一个 —— 它的核心思想就是"把算法的骨架定义在父类,具体步骤的实现交给子类"。Spring 的 JdbcTemplate / RestTemplate、Servlet 的 service 方法、Junit 的测试生命周期、几乎所有用 "Template" 命名的类背后都是它。这篇文章把模板方法讲透,讲清楚它和策略模式、装饰器、钩子的关系。
问题:多个类有相似流程但部分步骤不同
看一个常见场景:从不同数据源导入数据,流程都是"打开 → 读 → 转换 → 入库 → 关闭",但每一步具体实现不同:
class CsvImporter {
public void importData() {
// 1. 打开
BufferedReader r = new BufferedReader(new FileReader("data.csv"));
// 2. 读
List<String> lines = r.lines().collect(toList());
// 3. 转换
List<Record> records = lines.stream()
.map(l -> new Record(l.split(",")))
.collect(toList());
// 4. 入库
for (Record rec : records) db.save(rec);
// 5. 关闭
r.close();
}
}
class JsonImporter {
public void importData() {
InputStream s = new FileInputStream("data.json");
List<Map> jsons = new ObjectMapper().readValue(s, List.class);
List<Record> records = jsons.stream().map(Record::fromMap).collect(toList());
for (Record rec : records) db.save(rec);
s.close();
}
}
class XmlImporter { /* 类似但 XML 解析 */ }
问题:三个类有相同的"流程骨架",但每一步实现不同。代码大量重复 —— "入库""关闭"完全一样,"读"和"转换"只是格式不同。如果将来加"导入完成发邮件"这一步,要改三个类。
模板方法说:把骨架抽到父类,可变步骤声明为抽象方法,让子类填充。
模板方法的标准结构
// 抽象类:定义算法骨架(模板方法)
abstract class DataImporter {
// 模板方法:声明 final,防止子类覆盖整个流程
public final void importData() {
Object data = open();
List<Record> records = parse(data);
for (Record r : records) save(r);
close(data);
afterImport(); // 钩子,默认空
}
protected abstract Object open();
protected abstract List<Record> parse(Object data);
protected abstract void close(Object data);
// 通用步骤,父类实现
protected void save(Record r) {
db.save(r);
}
// 钩子方法:默认空实现,子类可选覆盖
protected void afterImport() {}
}
// 具体类:CSV 实现
class CsvImporter extends DataImporter {
private final String path;
public CsvImporter(String path) { this.path = path; }
protected Object open() {
try { return new BufferedReader(new FileReader(path)); }
catch (IOException e) { throw new RuntimeException(e); }
}
protected List<Record> parse(Object data) {
BufferedReader r = (BufferedReader) data;
return r.lines().map(l -> new Record(l.split(","))).collect(toList());
}
protected void close(Object data) {
try { ((BufferedReader) data).close(); } catch (Exception e) {}
}
}
class JsonImporter extends DataImporter {
private final String path;
public JsonImporter(String path) { this.path = path; }
protected Object open() {
try { return new FileInputStream(path); }
catch (Exception e) { throw new RuntimeException(e); }
}
protected List<Record> parse(Object data) {
try {
List<Map> raw = new ObjectMapper().readValue((InputStream) data, List.class);
return raw.stream().map(Record::fromMap).collect(toList());
} catch (Exception e) { throw new RuntimeException(e); }
}
protected void close(Object data) {
try { ((InputStream) data).close(); } catch (Exception e) {}
}
// 覆盖钩子:JSON 导入完发邮件
protected void afterImport() {
emailService.send("admin@x.com", "JSON 导入完成");
}
}
// 使用
new CsvImporter("data.csv").importData();
new JsonImporter("data.json").importData();
对比改造前后:
- 骨架在父类,加新步骤(如"导入前先备份")只改父类一处。
- 每个子类只关心自己"特殊"的部分,样板代码全部消失。
- 钩子方法让子类选择性扩展 —— 不覆盖就什么也不发生。
钩子方法 vs 抽象方法
这是模板方法的核心区分:
- 抽象方法:父类无法实现,子类必须实现。例:open / parse / close。
- 钩子方法:父类提供默认(通常是空),子类选择性覆盖。例:afterImport。
钩子让模板方法极其灵活 —— 子类不必关心所有扩展点,只覆盖自己关心的。这种"默认不动,需要时再插"的设计在框架里特别常见。
实战 1:Spring JdbcTemplate
// Spring JdbcTemplate.query 内部就是模板方法
public <T> List<T> query(String sql, RowMapper<T> rowMapper) {
// 模板:连接 -> 准备语句 -> 执行 -> 映射 -> 关闭
Connection conn = getConnection();
try (PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {
List<T> result = new ArrayList<>();
while (rs.next()) {
result.add(rowMapper.mapRow(rs, rs.getRow())); // 让客户端决定每行变成什么
}
return result;
} finally {
releaseConnection(conn);
}
}
// 使用:你只需要写 RowMapper(可变部分)
List<User> users = jdbcTemplate.query(
"SELECT id, name FROM users",
(rs, i) -> new User(rs.getLong("id"), rs.getString("name"))
);
这里有个小变体:Spring 用 RowMapper 作为参数(Strategy 风格)而不是子类化(经典模板方法风格)。两种实现都对,后者用组合替代继承,在 Java 8+ 用 Lambda 更轻量。
实战 2:Servlet
public abstract class HttpServlet {
// 模板方法
protected void service(HttpServletRequest req, HttpServletResponse resp) {
String method = req.getMethod();
switch (method) {
case "GET": doGet(req, resp); break;
case "POST": doPost(req, resp); break;
case "PUT": doPut(req, resp); break;
case "DELETE": doDelete(req, resp); break;
// ...
}
}
// 钩子(默认返回 405 Method Not Allowed,子类按需覆盖)
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
resp.sendError(405);
}
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
resp.sendError(405);
}
// ...
}
// 用户继承
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
resp.getWriter().write("Hello");
}
}
所有 Servlet 子类都遵循"GET → doGet,POST → doPost"这个模板。你只写自己关心的方法,其他自动 405。这是 Java Servlet 几十年保持稳定的设计基础。
实战 3:JUnit 测试生命周期
public abstract class TestCase {
public void runBare() throws Throwable {
setUp();
try {
runTest();
} finally {
tearDown();
}
}
protected void setUp() {} // 钩子
protected void tearDown() {} // 钩子
protected abstract void runTest();
}
public class MyTest extends TestCase {
private Database db;
protected void setUp() { db = new Database(); db.connect(); }
protected void tearDown() { db.close(); }
protected void runTest() { assert(db.query("...") != null); }
}
JUnit 早期版本用继承 + 模板方法。现在用注解(@BeforeEach / @AfterEach)更灵活,但底层执行流程依然是模板方法。
实战 4:Spring Boot 启动流程
SpringApplication.run() 就是一个巨型模板方法:
// 简化版
public ConfigurableApplicationContext run(String... args) {
// 1. 启动监听器
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
// 2. 准备环境
ConfigurableEnvironment environment = prepareEnvironment(listeners, args);
// 3. 创建 ApplicationContext
ConfigurableApplicationContext context = createApplicationContext();
// 4. 准备 context
prepareContext(context, environment, listeners, args);
// 5. 刷新 context(加载 Bean)
refreshContext(context);
// 6. 调用 Runners
callRunners(context, args);
// 7. 启动完成
listeners.started(context);
return context;
}
每一步都是钩子,可以通过实现 ApplicationContextInitializer、ApplicationRunner、CommandLineRunner、@PostConstruct 等扩展。Spring Boot 的"自动配置 + 可扩展"哲学就建立在这种模板方法 + 钩子点的设计上。
模板方法 vs 策略
两者都解决"算法可变"的问题,但实现机制不同:
- 策略:组合。Context 持有 Strategy 对象,运行时可换。
- 模板方法:继承。子类继承父类,编译期就定了。
选择标准:
- 整个算法都可换 → 策略。
- 算法骨架固定,只有几步可变 → 模板方法。
- 需要运行时切换 → 策略。
- 父类有大量公共代码可复用 → 模板方法。
现代实践更倾向于策略(组合优于继承),但模板方法在"框架定义流程 + 用户填空"的场景里仍然不可替代。
反转控制(IoC)的核心思想就在这
模板方法体现了"好莱坞原则"(Hollywood Principle):"Don't call us, we'll call you"。客户端不主动调用框架,而是实现框架定义的钩子,框架在合适时机回调。这种"控制权倒置"是所有框架区别于普通库的根本特征。
Spring、Hibernate、JUnit、Servlet、Android SDK 之所以叫"框架",就是因为它们用模板方法定义了流程,让你在流程的各个钩子点填代码。
钩子点的设计原则
- 钩子要够多,让用户能在关键节点扩展(before/after/around)。
- 钩子要够少,避免接口爆炸 —— 5-10 个钩子是合理范围。
- 钩子有合理默认,不强制子类全部实现。
- 钩子的顺序和契约要明确文档化:setUp 在 runTest 前,tearDown 在 runTest 后(即使 runTest 抛异常)。
各语言里的模板方法
Python:abstractmethod
from abc import ABC, abstractmethod
class DataImporter(ABC):
def import_data(self): # 模板方法
data = self.open()
records = self.parse(data)
for r in records: self.save(r)
self.close(data)
self.after_import()
@abstractmethod
def open(self): ...
@abstractmethod
def parse(self, data): ...
@abstractmethod
def close(self, data): ...
def save(self, r): db.save(r)
def after_import(self): pass # 钩子
TypeScript:abstract
abstract class DataImporter {
importData() {
const data = this.open();
const records = this.parse(data);
for (const r of records) this.save(r);
this.close(data);
this.afterImport();
}
protected abstract open(): any;
protected abstract parse(data: any): Record[];
protected abstract close(data: any): void;
protected save(r: Record) { db.save(r); }
protected afterImport() {} // 钩子
}
Go:没有继承,用结构体嵌套 + 接口
// Go 没有"模板方法"的语法表达,常见折中:接口里塞一个回调
type Importer interface {
Open() (io.Reader, error)
Parse(r io.Reader) ([]Record, error)
Close(r io.Reader) error
}
func RunImport(i Importer) error {
r, err := i.Open()
if err != nil { return err }
defer i.Close(r)
records, err := i.Parse(r)
if err != nil { return err }
for _, rec := range records { db.Save(rec) }
return nil
}
模板方法的常见坑
坑 1:模板方法不 final,被子类覆盖。 子类覆盖了 importData 本身,整个骨架就废了。Java 里务必 final;Python / JS 没有 final,只能靠文档约定。
坑 2:钩子方法暴露过多内部细节。 钩子参数让子类访问到不该访问的内部对象,污染封装。规则:钩子参数尽量"窄",传值不传引用。
坑 3:继承层次过深。 父 → 子 → 孙子三层模板方法,孙子要同时满足父和子的契约,改一个都可能影响。建议继承层次不超过 2 层。
坑 4:替代 if 不彻底。 父类还在 importData 里写 if (csv) {...} else (json) {...}。这就退化成普通函数了。模板方法的精髓是用多态代替条件分支。
坑 5:钩子被错误时机调用。 tearDown 应该在 runTest 抛异常时也执行,但简单实现里可能跳过。要用 try-finally 保证。
识别模板方法场景
- 有多个类执行相似流程。
- 流程骨架稳定,只有几个步骤可变。
- 变体之间有公共代码。
- 需要让用户/子类插入扩展逻辑(钩子)。
写在最后
模板方法是"框架"这个概念的祖师爷之一。它把"做事的顺序"固化在父类,把"具体怎么做每一步"开放给子类,这种"骨架不变、细节可填"的设计哲学贯穿所有大型软件。Spring、JUnit、Servlet 之所以让你的代码"插进去就能跑",就是因为它们用模板方法 + 钩子定义了清晰的扩展点。
给一个工程信号:当你发现自己在多个类里写几乎相同的流程,只有中间几步有差别时 —— 模板方法在向你招手。抽出父类,把变化点声明为抽象方法或钩子。代码会立刻清爽,且未来加新变体时"只填空,不动骨架"。在 Lambda 流行的今天,策略模式或许更轻量,但模板方法在"流程框架"领域仍然是不可替代的工具。
—— 别看了 · 2026