模板方法模式:算法骨架的艺术
说实话,模板方法模式是我在实际项目中用得最多的设计模式之一。为什么?因为它太实用了。你想想看,我们写代码时经常遇到这种情况:多个业务流程大体相同,只是某些步骤的实现不一样。如果每个流程都从头写一遍,那代码冗余得让人崩溃。
模板方法模式的核心思想很简单:在父类中定义算法的骨架,把一些步骤延迟到子类中实现。说白了,就是「流程我定好了,具体怎么做你们看着办」。
模式结构
我习惯把模板方法模式拆成三个角色来看:
- 抽象模板类:定义算法骨架,包含模板方法和基本方法
- 具体模板类:实现抽象方法,完成具体步骤
- 客户端:调用模板方法,触发整个流程
这里有个关键点:模板方法通常用 final 修饰,防止子类重写。为什么?因为骨架一旦被破坏,整个流程就乱套了。我在项目中见过有人把模板方法重写了,结果业务逻辑全乱掉,排查了半天才发现问题。
钩子方法:给子类留个「开关」
模板方法模式里有个很有意思的概念——钩子方法。它是个空方法或者有默认实现的方法,子类可以选择性地重写它。
举个例子:
public abstract class DataProcessor {
// 模板方法,final修饰防止重写
public final void process() {
loadData();
if (needValidate()) { // 钩子方法
validate();
}
processData();
saveResult();
}
protected abstract void loadData();
protected abstract void processData();
protected abstract void saveResult();
// 钩子方法,默认需要校验
protected boolean needValidate() {
return true;
}
private void validate() {
System.out.println("执行数据校验...");
}
}
public class FastDataProcessor extends DataProcessor {
@Override
protected void loadData() {
System.out.println("快速加载数据");
}
@Override
protected void processData() {
System.out.println("快速处理数据");
}
@Override
protected void saveResult() {
System.out.println("保存结果");
}
// 重写钩子,跳过校验
@Override
protected boolean needValidate() {
return false;
}
}
你看,needValidate() 就是个钩子。子类通过重写它,可以控制算法流程中的某个环节是否执行。我曾经在一个支付系统中用过这个技巧:普通支付需要风控校验,但内部转账可以跳过,一个钩子就搞定了。
JdbcTemplate:模板方法模式的经典应用
说到模板方法模式,Spring 的 JdbcTemplate 是个绕不开的例子。我当年第一次看 JdbcTemplate 源码时,心里就一个想法:原来模板方法可以这么用!
传统的 JDBC 操作有多繁琐?你想想看:获取连接、创建语句、执行查询、处理结果集、关闭连接、处理异常……每一步都要自己写,而且还得保证资源正确释放。稍不留神就忘了关闭连接,导致连接泄漏。
JdbcTemplate 把这一切封装好了。它的核心流程是这样的:
public class JdbcTemplate {
// 模板方法
public <T> T query(String sql, RowMapper<T> rowMapper) {
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
try {
conn = getConnection();
stmt = conn.createStatement();
rs = stmt.executeQuery(sql);
// 这里调用了抽象方法
return rowMapper.mapRow(rs);
} catch (SQLException e) {
handleException(e);
return null;
} finally {
// 资源释放由模板统一处理
closeResultSet(rs);
closeStatement(stmt);
closeConnection(conn);
}
}
}
你只需要实现 RowMapper 接口,告诉 JdbcTemplate 怎么把结果集映射成对象。连接管理、异常处理、资源释放这些脏活累活,模板方法全包了。
核心要点:模板方法模式把「不变的部分」封装在父类,「变化的部分」留给子类。JdbcTemplate 就是这种思想的完美体现——数据库操作流程是固定的,但结果映射逻辑是变化的。
实战:构建一个简易的 JdbcTemplate
光说不练假把式。我们来手写一个简化版的 JdbcTemplate,感受一下模板方法模式的魅力。
// 抽象模板类
public abstract class AbstractJdbcTemplate {
// 模板方法:执行查询
public final <T> List<T> executeQuery(String sql, RowMapper<T> rowMapper) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
List<T> result = new ArrayList<>();
try {
conn = getConnection();
ps = conn.prepareStatement(sql);
rs = ps.executeQuery();
while (rs.next()) {
// 钩子方法:是否继续读取
if (!shouldContinue()) {
break;
}
result.add(rowMapper.mapRow(rs));
}
// 钩子方法:记录日志
logQuery(sql, result.size());
return result;
} catch (SQLException e) {
handleException(e);
throw new RuntimeException("查询失败", e);
} finally {
close(rs);
close(ps);
close(conn);
}
}
// 抽象方法:获取连接
protected abstract Connection getConnection();
// 钩子方法:是否继续读取
protected boolean shouldContinue() {
return true;
}
// 钩子方法:记录日志
protected void logQuery(String sql, int count) {
// 默认不记录日志
}
// 钩子方法:异常处理
protected void handleException(SQLException e) {
// 默认打印异常
e.printStackTrace();
}
private void close(AutoCloseable resource) {
if (resource != null) {
try {
resource.close();
} catch (Exception e) {
// 忽略关闭异常
}
}
}
}
// 具体模板类
public class UserDao extends AbstractJdbcTemplate {
private DataSource dataSource;
public UserDao(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
protected Connection getConnection() {
try {
return dataSource.getConnection();
} catch (SQLException e) {
throw new RuntimeException("获取连接失败", e);
}
}
// 重写钩子:记录慢查询
@Override
protected void logQuery(String sql, int count) {
if (count > 1000) {
System.out.println("慢查询警告: " + sql + " 返回 " + count + " 条记录");
}
}
public List<User> findAllUsers() {
String sql = "SELECT * FROM users";
return executeQuery(sql, rs -> {
User user = new User();
user.setId(rs.getLong("id"));
user.setName(rs.getString("name"));
user.setEmail(rs.getString("email"));
return user;
});
}
}
个人经验:我在实际项目中,经常用钩子方法来做性能监控。比如在模板方法里加一个 beforeExecute() 和 afterExecute() 钩子,子类可以重写它们来记录执行时间。这样既不影响主流程,又能灵活扩展。
模板方法 vs 策略模式
很多初学者会把模板方法和策略模式搞混。我简单说说我的理解:
| 对比维度 | 模板方法模式 | 策略模式 |
|---|---|---|
| 核心思想 | 固定算法骨架,子类实现可变步骤 | 定义算法族,可互相替换 |
| 关系 | 继承关系(is-a) | 组合关系(has-a) |
| 控制权 | 父类控制流程 | 客户端选择策略 |
| 适用场景 | 多个子类有相同的流程骨架 | 需要动态切换算法 |
说白了,模板方法是「流程我定,细节你填」;策略模式是「接口我定,实现你选」。两者不冲突,甚至可以结合使用。
避坑指南
我曾经踩过的坑:
- 模板方法过于庞大:一个模板方法里塞了十几个步骤,子类要重写七八个抽象方法。这种设计反而增加了使用成本。我建议模板方法里的步骤控制在 3-5 个,超过这个数就该考虑拆分了。
- 滥用钩子方法:钩子方法太多会让子类不知所措。我一般只留 1-2 个关键的钩子,其他的用默认实现就好。
- 忘记用 final 修饰模板方法:这个错误我犯过一次。子类重写了模板方法,导致整个流程被破坏。从那以后,我写模板方法第一件事就是加 final。
知识体系总览
下面这张图总结了模板方法模式的核心知识结构:
总结
模板方法模式是个很「实在」的模式。它不像有些模式那样花哨,但解决的是实实在在的代码复用问题。我这些年做项目,但凡遇到多个类有相似的流程,第一个想到的就是模板方法。
记住三个关键点:
- 模板方法用 final 保护,别让子类把骨架改乱了
- 钩子方法要克制,给子类留太多选择反而是负担
- 抽象方法粒度要适中,太粗了复用性差,太细了实现成本高
嗯,模板方法模式就聊到这儿。代码写得多了,你会发现很多设计模式其实就在我们身边,只是我们没意识到而已。