模板方法模式:算法骨架的艺术

说实话,模板方法模式是我在实际项目中用得最多的设计模式之一。为什么?因为它太实用了。你想想看,我们写代码时经常遇到这种情况:多个业务流程大体相同,只是某些步骤的实现不一样。如果每个流程都从头写一遍,那代码冗余得让人崩溃。

模板方法模式的核心思想很简单:在父类中定义算法的骨架,把一些步骤延迟到子类中实现。说白了,就是「流程我定好了,具体怎么做你们看着办」。

模式结构

我习惯把模板方法模式拆成三个角色来看:

  • 抽象模板类:定义算法骨架,包含模板方法和基本方法
  • 具体模板类:实现抽象方法,完成具体步骤
  • 客户端:调用模板方法,触发整个流程

这里有个关键点:模板方法通常用 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) 抽象方法 实现抽象方法 重写钩子方法 经典应用:JdbcTemplate 连接管理 异常处理 资源释放 结果映射(RowMapper)

总结

模板方法模式是个很「实在」的模式。它不像有些模式那样花哨,但解决的是实实在在的代码复用问题。我这些年做项目,但凡遇到多个类有相似的流程,第一个想到的就是模板方法。

记住三个关键点:

  • 模板方法用 final 保护,别让子类把骨架改乱了
  • 钩子方法要克制,给子类留太多选择反而是负担
  • 抽象方法粒度要适中,太粗了复用性差,太细了实现成本高

嗯,模板方法模式就聊到这儿。代码写得多了,你会发现很多设计模式其实就在我们身边,只是我们没意识到而已。

公众号:蓝海资料掘金营,微信deep3321