Appearance
模板方法模式 (Template Method)
一、定义
一句话概括:定义一个操作中算法的骨架,将一些步骤延迟到子类中实现。模板方法让子类可以不改变算法结构的情况下重新定义算法的某些步骤。
官方定义: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.
二、解决的问题
2.1 问题场景
在开发中,经常遇到多个类共享相同流程但细节不同的情况。例如:
- 数据库操作:获取连接 → 执行 SQL → 处理结果 → 释放资源(流程相同,SQL 不同)
- 饮品制作:烧水 → 冲泡 → 倒入杯中 → 加调料(流程相同,具体饮品不同)
- 文档生成:加载模板 → 填充数据 → 格式转换 → 输出(流程相同,模板不同)
- 测试框架:setUp → 执行测试 → tearDown(流程相同,测试内容不同)
2.2 不用模板方法模式会怎样?
java
// 反例:每个类重复流程代码
class CoffeeMaker {
public void makeCoffee() {
boilWater();
brewCoffee(); // 不同的步骤
pourInCup();
addSugarMilk(); // 不同的步骤
}
}
class TeaMaker {
public void makeTea() {
boilWater(); // 重复代码
steepTeaBag(); // 不同的步骤
pourInCup(); // 重复代码
addLemon(); // 不同的步骤
}
}
// 公共流程代码重复,容易遗漏步骤问题:
- 公共流程代码重复,违反 DRY 原则
- 修改流程需要修改所有子类
- 容易遗漏某个步骤
三、结构
3.1 角色组成
| 角色 | 说明 |
|---|---|
| AbstractClass(抽象类) | 定义模板方法(算法骨架),声明抽象方法让子类实现 |
| ConcreteClass(具体类) | 实现抽象方法,完成算法中特定的步骤 |
| Hook Method(钩子方法) | 可选方法,提供默认实现,子类可选择性重写 |
3.2 类图(ASCII)
┌──────────────────────┐
│ AbstractClass │
├──────────────────────┤
│ + templateMethod() │◄── final,定义算法骨架
│ - primitiveOp1() │◄── abstract,子类必须实现
│ - primitiveOp2() │◄── abstract,子类必须实现
│ + hookMethod() │◄── 默认空实现,子类可选重写
└──────────┬───────────┘
│
┌──────┴──────┐
│ │
▼ ▼
┌──────────┐┌──────────┐
│Concrete ││Concrete │
│ClassA ││ClassB │
├──────────┤├──────────┤
│-op1() ││-op1() │
│-op2() ││-op2() │
└──────────┘└──────────┘四、代码实现
4.1 基础实现
java
// ==================== 抽象类:饮品制作 ====================
abstract class BeverageMaker {
// 模板方法(final,防止子类重写算法骨架)
public final void makeBeverage() {
boilWater();
brew();
pourInCup();
if (customerWantsCondiments()) { // 钩子方法
addCondiments();
}
hook(); // 空钩子方法
}
// 公共步骤(所有子类共用)
private void boilWater() {
System.out.println("1. 烧水");
}
private void pourInCup() {
System.out.println("3. 倒入杯中");
}
// 抽象方法(子类必须实现)
protected abstract void brew();
protected abstract void addCondiments();
// 钩子方法(默认空实现,子类可选择性重写)
protected void hook() { }
// 钩子方法(默认返回 true,子类可重写改变行为)
protected boolean customerWantsCondiments() {
return true;
}
}
// ==================== 具体类:咖啡 ====================
class CoffeeMaker extends BeverageMaker {
@Override
protected void brew() {
System.out.println("2. 用沸水冲泡咖啡");
}
@Override
protected void addCondiments() {
System.out.println("4. 加糖和牛奶");
}
@Override
protected void hook() {
System.out.println("5. 咖啡制作完成!");
}
}
// ==================== 具体类:茶 ====================
class TeaMaker extends BeverageMaker {
@Override
protected void brew() {
System.out.println("2. 用沸水浸泡茶叶");
}
@Override
protected void addCondiments() {
System.out.println("4. 加柠檬");
}
@Override
protected boolean customerWantsCondiments() {
// 询问用户是否需要加柠檬
return true; // 简化处理
}
}
// ==================== 客户端 ====================
public class TemplateMethodDemo {
public static void main(String[] args) {
System.out.println("=== 制作咖啡 ===");
BeverageMaker coffee = new CoffeeMaker();
coffee.makeBeverage();
System.out.println("\n=== 制作茶 ===");
BeverageMaker tea = new TeaMaker();
tea.makeBeverage();
}
}4.2 进阶实现
4.2.1 钩子方法详解
java
// 钩子方法的应用场景
abstract class DataProcessor {
// 模板方法
public final void process() {
connect();
if (validate()) { // 钩子:验证数据
fetchData();
transform();
if (needPersistence()) { // 钩子:是否需要持久化
save();
}
}
disconnect();
afterProcess(); // 钩子:后置处理
}
protected abstract void connect();
protected abstract void fetchData();
protected abstract void transform();
// 钩子方法:提供默认实现
protected boolean validate() { return true; }
protected boolean needPersistence() { return true; }
protected void save() { }
protected void disconnect() { }
protected void afterProcess() { }
}
// 具体实现:从数据库读取数据并导出
class DbDataExportProcessor extends DataProcessor {
@Override
protected void connect() {
System.out.println("连接数据库");
}
@Override
protected void fetchData() {
System.out.println("查询数据");
}
@Override
protected void transform() {
System.out.println("转换为 CSV 格式");
}
@Override
protected void save() {
System.out.println("保存到文件");
}
@Override
protected boolean validate() {
// 检查数据源是否可用
return true;
}
}4.2.2 好莱坞原则(Don't call us, we'll call you)
java
// 模板方法模式体现了"好莱坞原则"
// 父类调用子类方法,而不是子类调用父类方法
abstract class Game {
// 模板方法控制流程
public final void play() {
initialize();
while (!gameOver()) {
playTurn();
}
showResult();
}
protected abstract void initialize();
protected abstract void playTurn();
protected abstract boolean gameOver();
protected abstract void showResult();
}
class Chess extends Game {
private int turn = 0;
@Override
protected void initialize() {
System.out.println("初始化棋盘");
}
@Override
protected void playTurn() {
System.out.println("玩家 " + (turn % 2 + 1) + " 走棋");
turn++;
}
@Override
protected boolean gameOver() {
return turn >= 10; // 简化处理
}
@Override
protected void showResult() {
System.out.println("游戏结束");
}
}4.2.3 Spring JdbcTemplate 源码分析
java
// Spring JdbcTemplate 是模板方法模式的经典应用
// 简化版 JdbcTemplate 实现
class SimpleJdbcTemplate {
private DataSource dataSource;
public SimpleJdbcTemplate(DataSource dataSource) {
this.dataSource = dataSource;
}
// 模板方法:查询操作
public <T> T query(String sql, RowMapper<T> rowMapper, Object... params) {
Connection conn = null;
PreparedStatement stmt = null;
ResultSet rs = null;
try {
// 1. 获取连接(固定步骤)
conn = dataSource.getConnection();
// 2. 创建 Statement(固定步骤)
stmt = conn.prepareStatement(sql);
// 3. 设置参数(固定步骤)
setParameters(stmt, params);
// 4. 执行查询(固定步骤)
rs = stmt.executeQuery();
// 5. 映射结果(可变步骤,由 RowMapper 决定)
return mapResult(rs, rowMapper);
} catch (SQLException e) {
throw new RuntimeException("查询失败", e);
} finally {
// 6. 释放资源(固定步骤)
closeResource(rs, stmt, conn);
}
}
// 模板方法:更新操作
public int update(String sql, Object... params) {
Connection conn = null;
PreparedStatement stmt = null;
try {
conn = dataSource.getConnection();
stmt = conn.prepareStatement(sql);
setParameters(stmt, params);
return stmt.executeUpdate(); // 可变步骤
} catch (SQLException e) {
throw new RuntimeException("更新失败", e);
} finally {
closeResource(null, stmt, conn);
}
}
private void setParameters(PreparedStatement stmt, Object[] params)
throws SQLException {
for (int i = 0; i < params.length; i++) {
stmt.setObject(i + 1, params[i]);
}
}
private <T> T mapResult(ResultSet rs, RowMapper<T> rowMapper)
throws SQLException {
if (rs.next()) {
return rowMapper.mapRow(rs, 1);
}
return null;
}
private void closeResource(ResultSet rs, Statement stmt, Connection conn) {
try { if (rs != null) rs.close(); } catch (SQLException e) { }
try { if (stmt != null) stmt.close(); } catch (SQLException e) { }
try { if (conn != null) conn.close(); } catch (SQLException e) { }
}
}
// RowMapper 接口(策略模式)
interface RowMapper<T> {
T mapRow(ResultSet rs, int rowNum) throws SQLException;
}
// 使用 JdbcTemplate
public class JdbcTemplateDemo {
public static void main(String[] args) {
SimpleJdbcTemplate jdbcTemplate = new SimpleJdbcTemplate(null);
// 查询:RowMapper 是可变部分
User user = jdbcTemplate.query("SELECT * FROM users WHERE id = ?",
(rs, rowNum) -> new User(
rs.getLong("id"),
rs.getString("name"),
rs.getInt("age")
),
1
);
// 更新:SQL 和参数是可变部分
int rows = jdbcTemplate.update(
"UPDATE users SET name = ? WHERE id = ?", "张三", 1);
}
}4.3 生产级实现
Spring Boot 文件导入模板
java
// ==================== 抽象模板:文件导入处理 ====================
abstract class FileImportTemplate<T> {
// 模板方法
public final ImportResult importFile(MultipartFile file) {
long startTime = System.currentTimeMillis();
ImportResult result = new ImportResult();
try {
// 1. 文件校验
validateFile(file);
// 2. 解析文件
List<T> dataList = parseFile(file);
result.setTotalCount(dataList.size());
// 3. 数据校验
List<T> validData = validateData(dataList);
result.setValidCount(validData.size());
// 4. 数据转换
List<T> transformedData = transform(validData);
// 5. 批量保存
int savedCount = batchSave(transformedData);
result.setSuccessCount(savedCount);
// 6. 后置处理
afterImport(result);
} catch (Exception e) {
result.setSuccess(false);
result.setErrorMessage(e.getMessage());
handleError(e, file);
} finally {
result.setDuration(System.currentTimeMillis() - startTime);
}
return result;
}
// 抽象方法:子类实现
protected abstract void validateFile(MultipartFile file);
protected abstract List<T> parseFile(MultipartFile file);
protected abstract List<T> validateData(List<T> dataList);
protected abstract List<T> transform(List<T> dataList);
protected abstract int batchSave(List<T> dataList);
// 钩子方法
protected void afterImport(ImportResult result) {
System.out.println("导入完成,成功: " + result.getSuccessCount());
}
protected void handleError(Exception e, MultipartFile file) {
System.err.println("导入失败: " + e.getMessage());
}
}
// ==================== 具体实现:Excel 导入用户 ====================
@Component
class ExcelUserImportTemplate extends FileImportTemplate<UserDTO> {
@Override
protected void validateFile(MultipartFile file) {
String filename = file.getOriginalFilename();
if (filename == null || !filename.endsWith(".xlsx")) {
throw new IllegalArgumentException("仅支持 .xlsx 格式文件");
}
if (file.getSize() > 10 * 1024 * 1024) {
throw new IllegalArgumentException("文件大小不能超过 10MB");
}
}
@Override
protected List<UserDTO> parseFile(MultipartFile file) {
// 使用 Apache POI 解析 Excel
List<UserDTO> users = new ArrayList<>();
// ... 解析逻辑 ...
return users;
}
@Override
protected List<UserDTO> validateData(List<UserDTO> dataList) {
return dataList.stream()
.filter(user -> user.getName() != null && !user.getName().isEmpty())
.filter(user -> user.getAge() > 0 && user.getAge() < 150)
.collect(Collectors.toList());
}
@Override
protected List<UserDTO> transform(List<UserDTO> dataList) {
return dataList.stream()
.peek(user -> user.setName(user.getName().trim()))
.collect(Collectors.toList());
}
@Override
protected int batchSave(List<UserDTO> dataList) {
// 批量插入数据库
return dataList.size();
}
@Override
protected void afterImport(ImportResult result) {
// 发送导入完成通知
System.out.println("用户导入完成,共导入 " + result.getSuccessCount() + " 条");
}
}
// ==================== 导入结果 ====================
@Data
class ImportResult {
private boolean success = true;
private int totalCount;
private int validCount;
private int successCount;
private long duration;
private String errorMessage;
}
// ==================== Controller ====================
@RestController
@RequestMapping("/api/import")
class ImportController {
@Autowired private ExcelUserImportTemplate userImportTemplate;
@PostMapping("/users")
public ImportResult importUsers(@RequestParam("file") MultipartFile file) {
return userImportTemplate.importFile(file);
}
}五、优缺点
优点
- 代码复用:公共流程在父类中定义,子类只需实现差异部分
- 易于扩展:新增子类只需实现抽象方法,无需修改父类
- 控制反转:父类调用子类方法(好莱坞原则),控制权在父类
- 流程统一:所有子类遵循相同的算法骨架,确保流程一致性
缺点
- 继承限制:Java 单继承,子类只能继承一个模板父类
- 难以维护:算法骨架固定后,修改可能影响所有子类
- 违反里氏替换原则的风险:子类可能重写模板方法,破坏算法骨架
- 子类数量多:每个不同的实现都需要一个子类
六、适用场景
- 数据库操作:JdbcTemplate、HibernateTemplate
- 文件处理:文件导入/导出、格式转换
- 算法骨架:排序算法(比较步骤延迟到子类)
- 测试框架:JUnit 的 setUp/tearDown
- Servlet:HttpServlet 的 doGet/doPost
- 工作流引擎:定义固定的审批流程
七、JDK / Spring 框架中的实际应用
| 框架 | 应用位置 | 说明 |
|---|---|---|
| JDK | java.util.Arrays.sort() | 排序算法,比较步骤由 Comparator 决定 |
| JDK | java.io.InputStream.read() | 读取流程,read() 方法由子类实现 |
| JDK | javax.servlet.http.HttpServlet | service() 是模板方法,doGet/doPost 是抽象方法 |
| Spring | JdbcTemplate | 数据库操作模板,RowMapper 是可变部分 |
| Spring | RestTemplate | HTTP 请求模板 |
| Spring | TransactionTemplate | 事务管理模板 |
| Spring | AbstractApplicationContext | refresh() 是模板方法 |
| MyBatis | BaseExecutor | SQL 执行器模板 |
八、与其他模式的关系
与策略模式
- 模板方法模式:通过继承实现算法骨架,编译时确定行为,侧重"流程固定"
- 策略模式:通过组合实现算法替换,运行时切换行为,侧重"算法可替换"
- 两者可以结合使用:模板方法模式定义骨架,其中的可变步骤使用策略模式实现
与工厂方法模式
- 模板方法模式中经常使用工厂方法模式创建对象,工厂方法是模板方法的一个步骤
与好莱坞原则
- 模板方法模式是"好莱坞原则"(Don't call us, we'll call you)的典型体现
- 父类控制调用流程,子类只需提供具体实现
九、面试常见问题
Q1:模板方法模式和策略模式有什么区别?
A:模板方法模式使用继承,在父类中定义算法骨架,子类通过重写实现可变部分,行为在编译时确定;策略模式使用组合,在 Context 中持有 Strategy 接口,运行时可以切换策略,行为在运行时确定。模板方法更侧重"流程固定",策略模式更侧重"算法可替换"。
Q2:Spring JdbcTemplate 是如何使用模板方法模式的?
A:JdbcTemplate 定义了数据库操作的标准流程:获取连接 → 创建 Statement → 设置参数 → 执行 SQL → 处理结果集 → 释放资源。其中"执行 SQL"和"处理结果集"是可变部分,通过 PreparedStatementCreator、RowMapper、ResultSetExtractor 等回调接口实现。JdbcTemplate 结合了模板方法模式和策略模式。
Q3:什么是钩子方法(Hook Method)?它有什么作用?
A:钩子方法是模板方法模式中在父类中提供默认实现(通常是空实现)的方法,子类可以重写也可以不重写。作用:(1) 提供扩展点,让子类在不改变算法骨架的情况下控制某些行为;(2) 控制流程,如 customerWantsCondiments() 钩子可以决定是否执行某个步骤;(3) 降低子类实现负担,子类只需重写必要的方法。
Q4:为什么模板方法要声明为 final?
A:防止子类重写模板方法,破坏算法骨架。如果子类可以重写模板方法,就可能改变算法执行顺序、跳过关键步骤,导致系统行为不一致。声明为 final 确保了算法骨架的不可变性。如果确实需要子类修改流程,应该使用钩子方法提供扩展点。
Q5:模板方法模式中,父类如何防止子类重写不应该被重写的方法?
A:(1) 模板方法声明为 final;(2) 公共步骤声明为 private(子类不可见);(3) 抽象方法声明为 protected abstract(子类必须实现);(4) 钩子方法声明为 protected(子类可选重写)。通过合理的访问修饰符控制子类的重写权限。
