模板方法模式:数据迁移框架

说实话,模板方法模式是我在日常开发中用得最多的模式之一。为什么?因为它太实用了。你想想看,我们做系统架构时,经常遇到这样的场景:业务流程大体相同,但某些步骤的具体实现却千差万别。

模板方法模式的定义很简洁:定义一个操作中的算法骨架,而将一些步骤延迟到子类中。说白了,就是父类把流程定死,子类只管填空。

从 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 流程图:模板方法模式核心逻辑

模板方法模式 — 数据迁移框架核心流程 DataMigrationTemplate(父类) migrate() — 模板方法(final) connectSource() readData() transform() writeData() MySQL子类 Oracle子类 PostgreSQL子类 SQL Server子类 每个子类实现自己的 connectSource / readData / transform / writeData 但 migrate() 的执行流程由父类统一控制 cleanup() — 钩子方法(可选覆盖)

实际项目中的思考

我在做数据迁移框架时,还遇到过一个问题:如果迁移过程中某个步骤失败了,怎么办?比如数据读到一半,源库连接断了。

我的做法是在模板方法中加入重试机制和事务管理:

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