设计模式实战解读(三):模板方法模式——骨架复用与扩展点设计

本文是「设计模式实战解读」系列第三篇。系列文章统一按照 定义 → 痛点场景 → 模式结构 → 核心实现 → 真实应用 → 常见变种 → 优缺点 → 避坑指南 → FAQ 的结构展开,每篇聚焦一个模式讲透。


一句话定义

模板方法模式(Template Method):在父类中定义一个算法的骨架(步骤顺序),把某些步骤的具体实现延迟到子类中。子类可以重写步骤细节,但不能改变整体流程。

归属:行为型模式。


一、没有模板方法时的痛点

假设你在做一个数据同步模块,需要支持多种数据源的全量同步:

java 复制代码
// MySQL → 目标系统
public class MySQLSyncJob {
    public void execute() {
        log("开始同步");
        Connection conn = DriverManager.getConnection("jdbc:mysql://...");  // 建立连接
        ResultSet rs = conn.createStatement().executeQuery("SELECT * FROM orders"); // 拉取数据
        List<Map> data = convertToList(rs);     // 数据转换
        targetApi.batchPush(data);              // 推送到目标
        conn.close();                           // 关闭连接
        log("同步完成");
    }
}

// PostgreSQL → 目标系统
public class PostgresSyncJob {
    public void execute() {
        log("开始同步");
        Connection conn = DriverManager.getConnection("jdbc:postgresql://...");  // 建立连接
        ResultSet rs = conn.createStatement().executeQuery("SELECT * FROM orders"); // 拉取数据
        List<Map> data = convertToList(rs);     // 数据转换
        targetApi.batchPush(data);              // 推送到目标
        conn.close();                           // 关闭连接
        log("同步完成");
    }
}

// MongoDB → 目标系统
public class MongoSyncJob {
    public void execute() {
        log("开始同步");
        MongoClient client = MongoClients.create("mongodb://...");  // 建立连接
        FindIterable<Document> docs = client.getDatabase("db").getCollection("orders").find(); // 拉取
        List<Map> data = convertDocs(docs);     // 数据转换(不同!)
        targetApi.batchPush(data);              // 推送到目标
        client.close();                         // 关闭连接
        log("同步完成");
    }
}

三段代码的骨架完全相同(日志 → 连接 → 拉取 → 转换 → 推送 → 关闭 → 日志),只是具体步骤的实现不同。这就是典型的代码重复------流程是通用的,差异只在某几步。

问题:

  1. 骨架被重复了 N 遍,修改一处(比如加个耗时统计)要改 N 个文件
  2. 流程一致性无法保证------有人忘了关连接,有人忘了打日志
  3. 难以做统一的异常处理、监控埋点、重试逻辑

二、模式结构

复制代码
┌───────────────────────────────────┐
│ AbstractSyncJob (抽象父类)         │
├───────────────────────────────────┤
│ + execute()          ← 模板方法    │  流程骨架(final 不可重写)
│ # connect()          ← 抽象步骤    │  子类必须实现
│ # fetchData()        ← 抽象步骤    │  子类必须实现
│ # convertData()      ← 抽象步骤    │  子类必须实现
│ # pushData()         ← 具体步骤    │  父类默认实现(子类可选重写)
│ # close()            ← 抽象步骤    │  子类必须实现
│ # beforeExecute()    ← 钩子方法    │  默认空实现(子类可选重写)
│ # afterExecute()     ← 钩子方法    │  默认空实现(子类可选重写)
└───────────────────┬───────────────┘
                    │
        ┌───────────┼───────────┐
        ↓           ↓           ↓
  MySQLSyncJob  PostgresSyncJob  MongoSyncJob

三种方法类型:

  • 模板方法(Template Method) :定义流程骨架,通常标记 final 防止子类重写
  • 抽象步骤(Abstract Step):子类必须实现的变化点
  • 钩子方法(Hook):有默认空实现,子类按需重写

三、核心实现

3.1 基础版

java 复制代码
public abstract class AbstractSyncJob {
    
    // 模板方法:定义流程骨架(不允许子类修改流程顺序)
    public final void execute() {
        long start = System.currentTimeMillis();
        log("同步任务开始");
        
        beforeExecute();           // 钩子:执行前
        
        Object connection = connect();       // 步骤1:建立连接
        List<Map<String, Object>> rawData = fetchData(connection);  // 步骤2:拉取数据
        List<Map<String, Object>> converted = convertData(rawData); // 步骤3:转换数据
        pushData(converted);                 // 步骤4:推送数据
        close(connection);                   // 步骤5:关闭连接
        
        afterExecute();            // 钩子:执行后
        
        long cost = System.currentTimeMillis() - start;
        log("同步任务完成,耗时: " + cost + "ms");
    }
    
    // 抽象步骤(子类必须实现)
    protected abstract Object connect();
    protected abstract List<Map<String, Object>> fetchData(Object connection);
    protected abstract List<Map<String, Object>> convertData(List<Map<String, Object>> rawData);
    protected abstract void close(Object connection);
    
    // 具体步骤(有默认实现,子类可以重写)
    protected void pushData(List<Map<String, Object>> data) {
        targetApi.batchPush(data);
    }
    
    // 钩子方法(默认空实现)
    protected void beforeExecute() {}
    protected void afterExecute() {}
    
    private void log(String msg) {
        System.out.println("[" + getClass().getSimpleName() + "] " + msg);
    }
}

3.2 子类实现

java 复制代码
public class MySQLSyncJob extends AbstractSyncJob {
    private final String jdbcUrl;
    
    public MySQLSyncJob(String jdbcUrl) {
        this.jdbcUrl = jdbcUrl;
    }
    
    @Override
    protected Object connect() {
        return DriverManager.getConnection(jdbcUrl);
    }
    
    @Override
    protected List<Map<String, Object>> fetchData(Object connection) {
        Connection conn = (Connection) connection;
        // 执行 SQL,转成 List<Map>
        return JdbcUtils.queryForList(conn, "SELECT * FROM orders");
    }
    
    @Override
    protected List<Map<String, Object>> convertData(List<Map<String, Object>> rawData) {
        // MySQL 的字段名转换逻辑
        return rawData.stream()
            .map(row -> convertFieldNames(row, "mysql"))
            .collect(Collectors.toList());
    }
    
    @Override
    protected void close(Object connection) {
        ((Connection) connection).close();
    }
}

public class MongoSyncJob extends AbstractSyncJob {
    private final String mongoUri;
    
    @Override
    protected Object connect() {
        return MongoClients.create(mongoUri);
    }
    
    @Override
    protected List<Map<String, Object>> fetchData(Object connection) {
        MongoClient client = (MongoClient) connection;
        // MongoDB 查询逻辑
        return MongoUtils.findAll(client, "db", "orders");
    }
    
    @Override
    protected List<Map<String, Object>> convertData(List<Map<String, Object>> rawData) {
        // MongoDB 的 _id 转换、嵌套文档展平等
        return rawData.stream()
            .map(MongoFieldConverter::flatten)
            .collect(Collectors.toList());
    }
    
    @Override
    protected void close(Object connection) {
        ((MongoClient) connection).close();
    }
    
    // 重写钩子:MongoDB 同步前先检查集合是否存在
    @Override
    protected void beforeExecute() {
        checkCollectionExists();
    }
}

3.3 关键设计点

  1. execute()final:防止子类"不小心"改了流程顺序
  2. 抽象步骤用 protected abstract:强制子类实现
  3. 钩子用 protected(非 abstract):有默认空实现,子类选择性重写
  4. 模板方法内统一做耗时统计、异常处理------修改一处,所有子类生效

四、真实应用场景

4.1 框架级应用

框架 模板方法在哪 骨架步骤 子类扩展点
Spring AbstractApplicationContext.refresh() Bean 生命周期加载流程 onRefresh() 等钩子
Servlet HttpServlet.service() 解析请求类型→分发 doGet()/doPost()
JUnit TestCase.runBare() setUp→test→tearDown setUp()/tearDown()
MyBatis BaseExecutor.query() 缓存检查→查询→结果处理 doQuery()
Spring MVC DispatcherServlet.doDispatch() 请求映射→处理→视图渲染 HandlerAdapter
RocketMQ DefaultMQPushConsumer 拉取→过滤→消费→ACK consumeMessage()

4.2 业务场景

业务 骨架(不变的流程) 变化的步骤
数据同步 连接→拉取→转换→推送→关闭 连接方式、数据转换逻辑
导出报告 查数据→组装→渲染→写文件 渲染引擎(PDF/Excel/HTML)
审批流 提交→校验→通知→归档 校验规则、通知渠道
支付 参数组装→签名→请求→验签→返回 签名算法、请求格式
连接器执行 鉴权→构建请求→发送→解析响应→记录日志 鉴权方式、请求构造、响应解析
消息消费 接收→反序列化→校验→处理→ACK 处理逻辑

4.3 连接器 Handler 的模板方法

在 iPaaS 引擎中,每个连接器的执行流程是模板化的:

复制代码
AbstractConnectorHandler.execute()  ← 模板方法
    ├── 1. resolveAuth()           ← 解析鉴权信息(OAuth/APIKey/Basic)
    ├── 2. buildRequest()          ← 构建 HTTP/RPC 请求(子类实现)
    ├── 3. executeRequest()        ← 发送请求(通用 HTTP 客户端)
    ├── 4. parseResponse()         ← 解析响应(子类实现)
    ├── 5. handleError()           ← 错误处理(有默认实现,子类可重写)
    └── 6. recordLog()             ← 记录执行日志(通用)

新增一个连接器(比如对接飞书),只需要实现 buildRequest()parseResponse(),其他步骤由父类统一保证。这让连接器开发从"写全链路"变成"只写差异"。


五、常见变种

5.1 模板方法 + 策略模式

当变化的步骤可以独立抽象成策略时,可以用组合代替继承:

java 复制代码
public class SyncJob {
    private final DataFetcher fetcher;     // 策略1:拉取
    private final DataConverter converter; // 策略2:转换
    private final DataPusher pusher;       // 策略3:推送
    
    // 模板方法(流程固定,步骤委托给策略)
    public void execute() {
        Object data = fetcher.fetch();
        Object converted = converter.convert(data);
        pusher.push(converted);
    }
}

这种方式比纯继承更灵活------可以自由组合 fetcher/converter/pusher,不需要为每种组合创建子类。

5.2 带回调的模板方法

Java 8+ 可以用 Lambda 代替继承:

java 复制代码
public class SyncTemplate {
    
    public void execute(
        Supplier<Object> connect,
        Function<Object, List<Map<String, Object>>> fetch,
        UnaryOperator<List<Map<String, Object>>> convert,
        Consumer<Object> close
    ) {
        Object conn = connect.get();
        try {
            List<Map<String, Object>> data = fetch.apply(conn);
            List<Map<String, Object>> converted = convert.apply(data);
            pushData(converted);
        } finally {
            close.accept(conn);
        }
    }
}

// 使用
syncTemplate.execute(
    () -> DriverManager.getConnection(url),
    conn -> JdbcUtils.queryForList((Connection) conn, sql),
    data -> data.stream().map(this::convertFields).collect(toList()),
    conn -> ((Connection) conn).close()
);

优势:不需要定义子类,一次性使用更轻量。

5.3 多级模板方法

父类定义大骨架,中间层抽象类定义子骨架:

复制代码
AbstractHandler
    └── AbstractHttpHandler  (骨架: buildUrl → addHeaders → sendRequest → parseBody)
        ├── RestApiHandler
        └── GraphQLHandler
    └── AbstractMqHandler   (骨架: connectBroker → subscribe → consume → ack)
        ├── RocketMqHandler
        └── KafkaHandler

六、优缺点

优点 缺点
复用骨架代码,消除重复 子类受父类约束,灵活性有限
流程一致性有保障 继承层次深时可读性下降
统一做横切逻辑(日志/监控/异常) 模板方法越多,父类越臃肿
新增变种只需写差异部分 不了解父类全貌时容易误用
子类不能破坏整体流程 对组合友好度不如策略模式

七、避坑指南

坑 1:模板方法没标 final

不加 final,子类可能"重写"模板方法本身,破坏流程------导致某些步骤被跳过。务必给模板方法加 final

坑 2:步骤太多导致"超级父类"

当骨架步骤超过 7 个时,父类变得很难理解。解法:把步骤分组,用多级模板(中间层抽象类)或组合模式分解。

坑 3:抽象步骤和钩子的边界不清

  • 抽象步骤(abstract):子类必须实现的核心差异
  • 钩子(非 abstract + 空实现):子类可选增强的扩展点

如果搞反了------把钩子做成 abstract,子类被迫实现一堆空方法;把核心步骤做成钩子,子类忘了重写导致默认行为不对。

坑 4:子类之间有交叉逻辑

MySQLSyncJob 和 PostgresSyncJob 的 connect() 很像(都是 JDBC),但和 MongoSyncJob 完全不同。这时应该加一个中间层 AbstractJdbcSyncJob,把 JDBC 共性抽上去:

复制代码
AbstractSyncJob
    ├── AbstractJdbcSyncJob   (共享 JDBC connect/close)
    │       ├── MySQLSyncJob
    │       └── PostgresSyncJob
    └── MongoSyncJob

坑 5:模板方法内异常处理不统一

父类的 execute() 应该统一处理异常(try-catch + 清理资源),不要让子类各自处理。否则某些子类忘了 close 连接,就会泄漏。


八、常见问题(FAQ)

Q:模板方法和策略模式什么时候该用哪个?

A:如果变化的只是某一步的算法选择 ,用策略模式(组合);如果有多个步骤需要变化且这些步骤之间有顺序依赖,用模板方法(继承)。实际项目中经常结合使用------模板方法控制流程骨架,每一步内部可以委托给策略。

Q:模板方法的子类可以改变步骤顺序吗?

A:不应该改变。模板方法的价值就在于"流程不变,细节可变"。如果流程本身也需要变化,应该用策略模式或责任链模式。

Q:如何避免继承层次太深?

A:① 控制在 2-3 层以内;② 用 Lambda 回调代替子类(Java 8+ 场景);③ 用组合(策略)代替继承。如果发现继承超过 3 层,通常意味着需要重构。

Q:Spring 里大量使用模板方法吗?

A:是的。Spring 的核心设计哲学就是"框架控制流程,开发者填充细节"。JdbcTemplate.execute()AbstractApplicationContext.refresh()RestTemplate.doExecute() 都是模板方法。用 Spring 的人每天都在用模板方法,只是很多人没意识到。

Q:什么时候应该把步骤做成钩子而不是抽象方法?

A:当这个步骤"大部分子类不需要重写"时设为钩子(有默认空实现)。当这个步骤"每个子类都不一样、必须提供实现"时设为抽象方法。判断标准是"默认不做任何事"是否合理。


九、小结

模板方法模式的核心价值:把不变的流程锁死在父类,把变化的细节交给子类。

三个实践要点:

  1. 模板方法加 final------锁死流程顺序,防止子类越界
  2. 区分抽象步骤和钩子------必须实现的用 abstract,可选的给空实现
  3. 当继承变复杂时切换到组合------用 Lambda/策略代替子类

下一篇我们聊观察者模式------当一个对象状态变化需要通知多个关注方时,如何做到松耦合的事件分发。


标签:#设计模式 #模板方法 #TemplateMethod #行为型模式 #Java #继承 #钩子方法 #Spring #框架设计 #代码复用 #面向对象 #软件工程

相关推荐
workflower13 小时前
使用大语言模型处理用户需求
大数据·人工智能·设计模式·重构·动态规划
geovindu17 小时前
go: Generators Pattern
开发语言·后端·设计模式·golang·生成器模式
GuWenyue21 小时前
前端异步请求踩坑?3种方式搞定Ajax数据交互,从XHR到async/await
前端·javascript·设计模式
我登哥MVP1 天前
走进 Gang of Four 设计模式:装饰器模式
java·spring boot·设计模式·装饰器模式
秋漓1 天前
软件设计模式
设计模式
许彰午1 天前
36_Java设计模式之代理模式
java·设计模式·代理模式
许彰午1 天前
35_Java设计模式之工厂模式
java·开发语言·设计模式
uoKent1 天前
项目整理——设计模式
设计模式·软件需求
折哥的程序人生 · 物流技术专研1 天前
Java 23 种设计模式:从踩坑到精通 | 番外:编排器+策略模式在多平台电子面单中的实战(含性能压测)
设计模式·策略模式·代码重构·java设计模式·编排器·电子面单·从踩坑到精通
YXLY25282 天前
庭院大门选型方案:铝艺大门的五大设计模式与六大性能优势分析
设计模式