模板方法模式:数据迁移框架
说实话,模板方法模式是我在日常开发中用得最多的模式之一。为什么?因为它太实用了。你想想看,我们做系统架构时,经常遇到这样的场景:业务流程大体相同,但某些步骤的具体实现却千差万别。
模板方法模式的定义很简洁:定义一个操作中的算法骨架,而将一些步骤延迟到子类中。说白了,就是父类把流程定死,子类只管填空。
从 JdbcTemplate 说起
如果你用过 Spring,那一定对 JdbcTemplate 不陌生。它的 execute 方法就是模板方法模式的经典案例。
我刚开始用 Spring 时,总觉得这玩意儿有点多余。直接拿 Connection 执行 SQL 不就行了?直到有一次,我在一个项目里手动管理数据库连接,结果忘了关闭连接,导致线上连接池爆了……嗯,从那以后,我再也不敢手写 JDBC 了。
来看看 JdbcTemplate 的核心逻辑:
public class JdbcTemplate {
public void execute(String sql) {
Connection conn = null;
Statement stmt = null;
try {
// 1. 获取连接
conn = getConnection();
// 2. 创建语句
stmt = conn.createStatement();
// 3. 执行 SQL(子类实现)
doExecute(stmt, sql);
// 4. 提交事务
conn.commit();
} catch (Exception e) {
// 5. 回滚
conn.rollback();
throw e;
} finally {
// 6. 释放资源
close(conn, stmt);
}
}
// 子类实现具体执行逻辑
protected void doExecute(Statement stmt, String sql) {
stmt.execute(sql);
}
}
你看,获取连接、提交、回滚、释放资源这些步骤,父类全包了。子类只需要关心 doExecute 这一个方法。这就是模板方法模式的核心思想:把不变的流程抽象到父类,把变化的细节留给子类。
数据迁移框架实战
好,理论说完了,咱们来点实际的。假设我们要设计一个通用的数据迁移框架。业务场景是这样的:从源数据库读取数据,经过转换,写入目标数据库。
这个流程看起来简单,但实际做起来坑很多。我曾经接手过一个数据迁移项目,源库是 Oracle,目标库是 MySQL,中间还要做字段映射、类型转换、数据清洗……如果每个迁移任务都从头写,那得累死。
用模板方法模式,我们可以这样设计:
public abstract class DataMigrationTemplate {
// 模板方法,定义迁移骨架
public final void migrate() {
// 1. 连接源库
connectSource();
// 2. 读取数据
List<RawData> rawData = readData();
// 3. 转换数据
List<TargetData> targetData = transform(rawData);
// 4. 写入目标库
writeData(targetData);
// 5. 清理资源
cleanup();
}
// 以下为抽象方法,子类必须实现
protected abstract void connectSource();
protected abstract List<RawData> readData();
protected abstract List<TargetData> transform(List<RawData> rawData);
protected abstract void writeData(List<TargetData> targetData);
// 钩子方法,子类可选覆盖
protected void cleanup() {
// 默认实现:关闭连接等
System.out.println("执行默认清理...");
}
}
这里有个细节我想强调一下:migrate 方法我加了 final 关键字。为什么?因为我不希望子类重写整个流程。你想想看,如果子类把 migrate 给覆盖了,那模板方法模式就失去意义了。
子类实现示例
假设我们要从 MySQL 迁移到 PostgreSQL:
public class MySQLToPostgreSQLMigration extends DataMigrationTemplate {
@Override
protected void connectSource() {
System.out.println("连接 MySQL 数据库...");
// 实际代码:建立 JDBC 连接
}
@Override
protected List<RawData> readData() {
System.out.println("从 MySQL 读取数据...");
// 实际代码:执行 SELECT 查询
return new ArrayList<>();
}
@Override
protected List<TargetData> transform(List<RawData> rawData) {
System.out.println("转换数据格式...");
// 实际代码:字段映射、类型转换
return new ArrayList<>();
}
@Override
protected void writeData(List<TargetData> targetData) {
System.out.println("写入 PostgreSQL...");
// 实际代码:执行 INSERT 语句
}
@Override
protected void cleanup() {
System.out.println("关闭 MySQL 和 PostgreSQL 连接...");
}
}
你看,子类只需要关注四个步骤的具体实现。流程控制、异常处理、资源管理,全由父类搞定。
模板方法模式的核心要素
| 要素 | 说明 | 示例 |
|---|---|---|
| 模板方法 | 定义算法骨架,通常为 final | migrate() |
| 抽象方法 | 子类必须实现的步骤 | connectSource(), readData() |
| 具体方法 | 父类已实现的公共逻辑 | 日志记录、异常处理 |
| 钩子方法 | 子类可选覆盖,提供扩展点 | cleanup() |
核心要点:模板方法模式的关键在于「控制反转」——父类控制流程,子类提供实现。这符合好莱坞原则:「不要打电话给我们,我们会打给你。」
避坑指南
我曾经在一个项目中犯过这样的错误:把模板方法设计得过于复杂,里面塞了十几个抽象方法。结果子类实现起来痛苦不堪,每个子类都要重写一大堆方法,其中很多根本用不上。
后来我学乖了,总结了几条经验:
- 抽象方法不要太多:3-5个为宜,太多会增加子类的负担
- 善用钩子方法:对于可选步骤,用钩子方法代替抽象方法
- 模板方法尽量短小:一个模板方法只做一件事,不要搞成大杂烩
- 考虑使用回调:如果子类只需要定制一个步骤,用回调函数可能更灵活
我的习惯:在设计模板方法时,我会先写一个「最小可用版本」——只包含最核心的步骤。然后根据实际需求,逐步添加钩子方法。这样既保证了灵活性,又不会过度设计。
模板方法 vs 策略模式
很多初学者会把模板方法模式和策略模式搞混。我简单说一下区别:
- 模板方法:通过继承实现,父类控制流程,子类重写部分步骤
- 策略模式:通过组合实现,将算法封装成独立的策略对象,可以动态切换
说白了,模板方法是「整体流程固定,局部可变」;策略模式是「整体算法可变,但接口固定」。选哪个?看场景。如果流程基本不变,只是某些步骤的实现不同,用模板方法。如果整个算法都可能变化,用策略模式。
SVG 流程图:模板方法模式核心逻辑
实际项目中的思考
我在做数据迁移框架时,还遇到过一个问题:如果迁移过程中某个步骤失败了,怎么办?比如数据读到一半,源库连接断了。
我的做法是在模板方法中加入重试机制和事务管理:
public final void migrateWithRetry(int maxRetries) {
int retries = 0;
while (retries < maxRetries) {
try {
migrate();
return; // 成功则退出
} catch (Exception e) {
retries++;
if (retries >= maxRetries) {
throw new MigrationException("迁移失败,已重试" + maxRetries + "次", e);
}
System.out.println("第" + retries + "次重试...");
// 等待一段时间再重试
Thread.sleep(1000 * retries);
}
}
}
你看,重试逻辑也放到了父类中。子类完全不用关心这些「脏活累活」。
注意:模板方法模式虽然好用,但不要滥用。如果子类之间的差异太大,强行用模板方法会导致父类臃肿不堪。这时候,考虑用策略模式或者组合模式可能更合适。
最后说一句:设计模式不是银弹。模板方法模式最适合的场景是「流程固定,实现多变」。如果你发现流程本身也在不断变化,那就要重新审视你的设计了。
公众号:蓝海资料掘金营,微信deep3321