常见设计模式-模板方法模式

模板方法模式(Template Method Pattern)实战指南


一、模板方法模式概述

定义: 在父类中定义一个算法的骨架,将某些步骤的具体实现延迟到子类中。模板方法使得子类可以在不改变算法整体结构的前提下,重新定义算法的某些步骤。

解决什么问题: 多个类有相同的处理流程,但某些步骤的实现不同。如果每个类都独立写一套流程,会导致大量重复代码。

核心思想: 封装不变,扩展可变。 把不变的流程固定在父类,把可变的步骤交给子类实现。


二、模式结构

2.1 角色说明

角色 职责 说明
AbstractClass(抽象类) 定义算法骨架 包含一个模板方法和若干基本方法
ConcreteClass(具体子类) 实现抽象方法和钩子方法 是算法顶级逻辑的组成步骤

2.2 基本方法的三种类型

类型 定义 特点
抽象方法(Abstract Method) 由抽象类声明,由具体子类实现 子类必须实现,决定算法的关键步骤
具体方法(Concrete Method) 由抽象类声明并实现 子类可直接继承,也可覆盖
钩子方法(Hook Method) 在抽象类中已实现 一般为 isXxx() 返回 boolean,用于判断逻辑;或空方法供子类选择性重写

2.3 结构伪代码

java 复制代码
// 1. 抽象类 ------ 定义算法骨架
public abstract class AbstractClass {

    // 【模板方法】定义算法骨架,按顺序调用基本方法,通常声明为 final
    public final void templateMethod() {
        step1();            // 【具体方法】父类已实现,子类继承即可
        if (isNeedStep2()) { // 【钩子方法】用于判断,子类可选择性重写
            step2();        // 【抽象方法】由子类实现
        }
        step3();            // 【抽象方法】由子类实现
        hook();             // 【钩子方法】空方法,子类可选择性重写
    }

    // 【具体方法】父类已实现
    private void step1() {
        System.out.println("固定步骤1:初始化");
    }

    // 【抽象方法】子类必须实现
    protected abstract void step2();

    // 【抽象方法】子类必须实现
    protected abstract void step3();

    // 【钩子方法】默认返回 true,子类可重写改变流程
    protected boolean isNeedStep2() {
        return true;
    }

    // 【钩子方法】空方法,子类可选择性重写
    protected void hook() {
    }
}

// 2. 具体子类 A
public class ConcreteClassA extends AbstractClass {
    @Override
    protected void step2() {
        System.out.println("子类A:实现步骤2");
    }

    @Override
    protected void step3() {
        System.out.println("子类A:实现步骤3");
    }
    // isNeedStep2() 和 hook() 继承父类默认实现
}

// 3. 具体子类 B
public class ConcreteClassB extends AbstractClass {
    @Override
    protected void step2() {
        System.out.println("子类B:实现步骤2");
    }

    @Override
    protected void step3() {
        System.out.println("子类B:实现步骤3");
    }

    @Override
    protected boolean isNeedStep2() {
        return false;  // 重写钩子方法,跳过步骤2
    }

    @Override
    protected void hook() {
        System.out.println("子类B:重写了钩子方法");
    }
}

// 4. 客户端调用
AbstractClass objA = new ConcreteClassA();
objA.templateMethod();  // 输出:固定步骤1 → 子类A步骤2 → 子类A步骤3

AbstractClass objB = new ConcreteClassB();
objB.templateMethod();  // 输出:固定步骤1 → 子类B步骤3 → 子类B钩子

三、优缺点

优点

  1. 消除重复代码: 不变的流程固定在父类,子类只需关注差异化的步骤。
  2. 符合开闭原则: 新增子类无需修改父类算法骨架,只需实现或重写基本方法。
  3. 流程可控: 父类通过模板方法统一控制算法的执行顺序和条件。
  4. 钩子方法提供灵活性: 子类可通过重写钩子方法影响算法流程,而无需修改算法结构。

缺点

  1. 类数量增多: 每增加一种变体就需要新增一个子类。
  2. 继承关系强耦合: 子类和父类之间通过继承紧密绑定,父类的修改可能影响所有子类。
  3. 灵活性受限: 算法骨架固定在父类中,运行时无法动态切换流程。

四、适用场景

  • 流程固定,步骤可变: 多个类有相同的整体流程,但某些步骤的实现不同。
  • 代码重复: 多个类中存在重复的流程控制代码,只是具体处理逻辑不同。
  • 需要约束子类: 希望子类实现某些步骤,但不允许改变算法的整体执行顺序。
  • 典型场景: 数据导入导出、支付流程、测试框架(JUnit 的 setUp()runTest()tearDown())、Servlet 的 doGet()/doPost() 等。

五、实战分析

5.1 JdbcTemplate(Spring 经典案例)

问题背景

JDBC 原生代码中,每次执行 SQL 都需要重复编写:获取连接 → 创建 Statement → 设置参数 → 执行 SQL → 处理 ResultSet → 关闭资源。这些流程是固定的,但「获取连接的方式」和「对查询结果的处理」是可变的。Spring 的 JdbcTemplate 用模板方法模式解决了这个问题。

模板方法(算法骨架)

Spring 源码中 JdbcTemplate 的核心模板方法 execute()

java 复制代码
// 【抽象类】JdbcTemplate ------ 定义算法骨架
public class JdbcTemplate {

    // 【模板方法】定义了 JDBC 操作的完整流程,按固定顺序调用各步骤
    public <T> T execute(StatementCallback<T> action) {
        // 【具体方法】步骤1:获取数据库连接(父类已实现)
        Connection con = DataSourceUtils.getConnection(obtainDataSource());
        Statement stmt = null;
        try {
            // 【具体方法】步骤2:创建 Statement(父类已实现)
            stmt = con.createStatement();
            // 【具体方法】步骤3:设置查询超时(父类已实现)
            applyStatementSettings(stmt);
            // 【抽象方法】步骤4:执行 SQL ------ 由子类/回调实现
            T result = action.doInStatement(stmt);
            // 【具体方法】步骤5:处理结果(父类已实现)
            handleResults(stmt);
            return result;
        } catch (SQLException ex) {
            // 【具体方法】步骤6:异常处理(父类已实现)
            throw translateException("StatementCallback", sql, ex);
        } finally {
            // 【具体方法】步骤7:关闭资源(父类已实现)
            JdbcUtils.closeStatement(stmt);
            DataSourceUtils.releaseConnection(con, getDataSource());
        }
    }
}

角色对应:

  • JdbcTemplate = 抽象类(虽然不是 abstract 修饰,但承担抽象类角色)
  • execute() = 模板方法(定义算法骨架,按顺序调用各步骤)
  • con.createStatement() / applyStatementSettings() / handleResults() = 具体方法(父类已实现)
  • action.doInStatement(stmt) = 抽象方法(由调用方通过回调实现)
  • translateException() = 具体方法(父类已实现的异常处理)
子类如何使用(回调方式)

JdbcTemplate 没有通过继承来扩展,而是通过回调接口让调用方定义差异化的步骤:

java 复制代码
// 查询操作 ------ 调用方实现【抽象方法】doInStatement
List<User> users = jdbcTemplate.execute((Statement stmt) -> {
    ResultSet rs = stmt.executeQuery("SELECT * FROM user");
    List<User> list = new ArrayList<>();
    while (rs.next()) {
        list.add(mapRowToUser(rs));  // 调用方定义如何处理每行数据
    }
    return list;
});

// 更新操作 ------ 另一个调用方实现【抽象方法】doInStatement
int rows = jdbcTemplate.execute((Statement stmt) -> {
    return stmt.executeUpdate("UPDATE user SET name = 'test' WHERE id = 1");
});
整体流程
复制代码
调用方调用 jdbcTemplate.execute(回调)
  ↓
【模板方法】execute() 开始
  ↓
【具体方法】获取数据库连接
  ↓
【具体方法】创建 Statement
  ↓
【抽象方法】回调.doInStatement(stmt) ← 调用方实现差异化逻辑
  ↓
【具体方法】处理结果 / 异常处理 / 关闭资源
  ↓
返回结果

5.2 支付流程(业务实战案例)

问题背景

电商/金融系统中,支付流程通常包含:创建支付订单 → 调用支付渠道 → 处理回调 → 更新订单状态 → 发送通知。这些步骤的顺序是固定的,但「调用支付渠道」和「处理回调」的具体逻辑因渠道不同而不同(微信支付、支付宝支付、银联支付等)。

模板方法(算法骨架)
java 复制代码
// 【抽象类】AbstractPayService ------ 定义支付流程骨架
public abstract class AbstractPayService {

    /**
     * 【模板方法】支付流程骨架,声明为 final 防止子类修改流程顺序
     * 固定步骤:验证 → 创建订单 → 调用支付 → 处理回调 → 更新状态 → 通知
     */
    public final PayResult pay(PayRequest request) {
        // 【具体方法】步骤1:参数校验(父类已实现,子类继承即可)
        validateRequest(request);

        // 【抽象方法】步骤2:创建支付订单 ------ 子类必须实现
        PayOrder order = createPayOrder(request);

        // 【抽象方法】步骤3:调用支付渠道 ------ 子类必须实现(核心差异点)
        PayResponse response = doPay(order);

        // 【钩子方法】步骤4:是否需要处理回调 ------ 子类可选择性重写
        if (needHandleCallback()) {
            handleCallback(response);
        }

        // 【具体方法】步骤5:更新订单状态(父类已实现)
        updateOrderStatus(order, response);

        // 【钩子方法】步骤6:发送通知 ------ 子类可选择性重写
        afterPayNotify(order, response);

        // 【具体方法】步骤7:返回结果(父类已实现)
        return buildResult(order, response);
    }

    // ==================== 基本方法 ====================

    // 【具体方法】参数校验 ------ 父类已实现
    private void validateRequest(PayRequest request) {
        if (request == null || request.getAmount() == null) {
            throw new ServiceException("支付参数不能为空");
        }
        if (request.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
            throw new ServiceException("支付金额必须大于0");
        }
    }

    // 【抽象方法】创建支付订单 ------ 子类必须实现
    protected abstract PayOrder createPayOrder(PayRequest request);

    // 【抽象方法】调用支付渠道 ------ 子类必须实现(核心差异点)
    protected abstract PayResponse doPay(PayOrder order);

    // 【具体方法】处理回调 ------ 父类已实现
    protected void handleCallback(PayResponse response) {
        log.info("支付回调处理完成,交易号:{}", response.getTransactionNo());
    }

    // 【具体方法】更新订单状态 ------ 父类已实现
    private void updateOrderStatus(PayOrder order, PayResponse response) {
        if (response.isSuccess()) {
            order.setStatus(PayStatus.PAID);
        } else {
            order.setStatus(PayStatus.FAILED);
        }
        orderService.updateById(order);
    }

    // 【钩子方法】是否需要处理回调 ------ 默认返回 true,子类可重写
    protected boolean needHandleCallback() {
        return true;
    }

    // 【钩子方法】支付后通知 ------ 默认空实现,子类可选择性重写
    protected void afterPayNotify(PayOrder order, PayResponse response) {
    }

    // 【具体方法】构建返回结果 ------ 父类已实现
    private PayResult buildResult(PayOrder order, PayResponse response) {
        PayResult result = new PayResult();
        result.setOrderId(order.getId());
        result.setSuccess(response.isSuccess());
        result.setMessage(response.getMessage());
        return result;
    }
}

角色对应:

  • AbstractPayService = 抽象类(定义支付流程骨架)
  • pay() = 模板方法final 修饰,按固定顺序调用各步骤)
  • validateRequest() / updateOrderStatus() / buildResult() = 具体方法(父类已实现)
  • createPayOrder() / doPay() = 抽象方法(子类必须实现)
  • needHandleCallback() = 钩子方法(返回 boolean,用于条件判断)
  • afterPayNotify() = 钩子方法(空方法,子类可选择性重写)
具体子类实现
java 复制代码
// 【具体子类】微信支付
@Component("wechatPay")
public class WechatPayService extends AbstractPayService {

    @Override
    protected PayOrder createPayOrder(PayRequest request) {
        PayOrder order = new PayOrder();
        order.setOrderNo(generateOrderNo("WX"));
        order.setAmount(request.getAmount());
        order.setPayType("WECHAT");
        return order;
    }

    @Override
    protected PayResponse doPay(PayOrder order) {
        // 调用微信支付 API
        WechatPayRequest wxRequest = new WechatPayRequest();
        wxRequest.setTotalFee(order.getAmount().multiply(BigDecimal.valueOf(100)).intValue());
        wxRequest.setOutTradeNo(order.getOrderNo());
        WechatPayResponse wxResponse = wechatPayClient.unifiedOrder(wxRequest);
        // 转换为通用响应
        PayResponse response = new PayResponse();
        response.setSuccess("SUCCESS".equals(wxResponse.getReturnCode()));
        response.setTransactionNo(wxResponse.getTransactionId());
        return response;
    }

    // 微信支付不需要处理回调通知,重写【钩子方法】返回 false
    @Override
    protected boolean needHandleCallback() {
        return false;
    }
}

// 【具体子类】支付宝支付
@Component("alipay")
public class AlipayService extends AbstractPayService {

    @Override
    protected PayOrder createPayOrder(PayRequest request) {
        PayOrder order = new PayOrder();
        order.setOrderNo(generateOrderNo("ALI"));
        order.setAmount(request.getAmount());
        order.setPayType("ALIPAY");
        return order;
    }

    @Override
    protected PayResponse doPay(PayOrder order) {
        // 调用支付宝 API
        AlipayTradePayRequest aliRequest = new AlipayTradePayRequest();
        aliRequest.setTotalAmount(order.getAmount().toString());
        aliRequest.setOutTradeNo(order.getOrderNo());
        AlipayTradePayResponse aliResponse = alipayClient.execute(aliRequest);
        // 转换为通用响应
        PayResponse response = new PayResponse();
        response.setSuccess("10000".equals(aliResponse.getCode()));
        response.setTransactionNo(aliResponse.getTradeNo());
        return response;
    }

    // 支付宝支付需要发送通知,重写【钩子方法】
    @Override
    protected void afterPayNotify(PayOrder order, PayResponse response) {
        if (response.isSuccess()) {
            // 发送支付成功短信/微信通知
            notificationService.sendPaySuccess(order);
        }
    }
}
调用入口
java 复制代码
@Service
public class PayService {
    @Resource
    private Map<String, AbstractPayService> payServiceMap;  // Spring 自动注入所有子类

    public PayResult pay(String payType, PayRequest request) {
        AbstractPayService payService = payServiceMap.get(payType);
        if (payService == null) {
            throw new ServiceException("不支持的支付方式: " + payType);
        }
        return payService.pay(request);  // 调用模板方法
    }
}

// 业务调用
payService.pay("wechatPay", request);   // 走微信支付流程
payService.pay("alipay", request);      // 走支付宝流程
整体流程
复制代码
业务方调用 payService.pay("alipay", request)
  ↓
获取对应的子类 AlipayService
  ↓
调用模板方法 pay(request)
  ↓
【具体方法】validateRequest() → 参数校验
  ↓
【抽象方法】createPayOrder()  → 子类实现:创建支付宝订单
  ↓
【抽象方法】doPay()           → 子类实现:调用支付宝 API
  ↓
【钩子方法】needHandleCallback() → 子类可重写:是否处理回调
  ↓
【具体方法】updateOrderStatus() → 更新订单状态
  ↓
【钩子方法】afterPayNotify()   → 子类可重写:发送通知
  ↓
【具体方法】buildResult()      → 构建返回结果
  ↓
返回 PayResult

两者对比总结

对比项 JdbcTemplate 支付流程
抽象类 JdbcTemplate AbstractPayService
模板方法 execute() pay()
具体方法 获取连接、创建 Statement、关闭资源 参数校验、更新状态、构建结果
抽象方法 action.doInStatement()(回调) createPayOrder()doPay()
钩子方法 needHandleCallback()afterPayNotify()
扩展方式 回调接口(无需继承) 继承抽象类(子类重写)
典型场景 框架层代码,追求低耦合 业务层代码,追求易理解

六、模板方法 vs 策略模式

对比项 模板方法模式 策略模式
核心思想 封装不变的流程,扩展可变的步骤 封装可互换的算法
关系 父类与子类之间是继承关系 策略与上下文之间是组合关系
算法数量 一个算法骨架,多个子类变体 多个独立的算法
切换时机 编译时确定(子类类型) 运行时动态切换(通过引擎/工厂)
扩展方式 新增子类 新增策略实现类
流程控制 父类控制整体流程 上下文/引擎控制策略选择
适用场景 流程固定,步骤可变 多个算法可互换

如何选择:

  • 如果多个类有相同的处理流程 ,只是某些步骤不同 → 模板方法
  • 如果同一个操作有多种完全不同的实现 ,需要运行时切换 → 策略模式

七、面试高频考点

Q1:模板方法模式中模板方法为什么要声明为 final?

答: 防止子类修改算法的执行顺序。模板方法的核心价值是固定流程,如果子类重写了模板方法,就破坏了算法的稳定性,违背了模板方法模式的初衷。

Q2:模板方法的三种基本方法有什么区别?

答:

  • 抽象方法: 父类只声明,子类必须实现。决定算法的关键差异化步骤。
  • 具体方法: 父类已实现,子类继承即可。适用于所有子类都相同的步骤。
  • 钩子方法: 父类已实现(通常为空或返回默认值),子类可选择性重写。用于条件控制(如 isXxx() 返回 boolean)或可选的扩展点。

Q3:JdbcTemplate 为什么用回调而不是继承?

答: 回调方式比继承更灵活:

  1. 避免类爆炸: 继承方式需要为每种 SQL 操作创建子类;回调方式只需传入不同的回调对象。
  2. 低耦合: 调用方不需要继承框架类,只需实现回调接口。
  3. 可组合: 同一个 JdbcTemplate 实例可以配合不同的回调使用。

Q4:模板方法和策略模式有什么区别?怎么选择?

答:

  • 模板方法关注的是「流程相同,步骤不同」,通过继承来扩展。
  • 策略模式关注的是「算法可互换」,通过组合来切换。
  • 如果有固定的执行流程,只是某些步骤需要自定义 → 用模板方法。
  • 如果有多种完全不同的实现,需要在运行时切换 → 用策略模式。

Q5:钩子方法有什么作用?

答: 钩子方法为子类提供了不影响算法结构的扩展能力

  1. 条件控制: 返回 boolean 控制某步骤是否执行,如 needHandleCallback()
  2. 空操作: 默认不做任何事情,子类需要时再重写,如 afterPayNotify()
  3. 降低耦合: 子类不需要的步骤不用强制实现。

Q6:模板方法模式的缺点是什么?怎么缓解?

答:

  • 缺点 1:类数量增多。 缓解方式:合理使用回调接口代替继承(如 JdbcTemplate)。
  • 缺点 2:继承强耦合。 缓解方式:父类尽量只包含稳定的公共逻辑,减少对子类的影响。
  • 缺点 3:流程不灵活。 缓解方式:如果需要运行时切换流程,改用策略模式。

八、总结

模板方法模式的本质是封装不变的流程,扩展可变的步骤

JdbcTemplate 和支付流程两个案例,都遵循了同一个模式:

复制代码
抽象类定义模板方法(final)
  ↓
具体方法:父类实现,子类继承
  ↓
抽象方法:子类必须实现
  ↓
钩子方法:子类可选择性重写

掌握了这个套路,你就能在自己的项目中灵活运用模板方法模式------当发现多个类有重复的流程代码时,把不变的流程提取到父类,把可变的步骤留给子类。

相关推荐
Wang's Blog1 小时前
Java 项目实战: 外卖平台-软件开发流程与项目整体介绍
java·服务器·redis
步行cgn1 小时前
BeanFactory 与 FactoryBean 的区别:面试深度解析
java·后端·spring
IT 行者1 小时前
JDK 27 正式发布:内存、安全、并发三路齐发
java
伍一514 小时前
hiprint-spring-boot打印组件介绍
java·hiprint
Wang's Blog9 小时前
Java 接入Redis: 服务启动停止与后台运行配置
java·服务器·redis
此时不提桶,更待何时10 小时前
01-04-B-垃圾回收面试与生产事故实战
java·jvm·面试
xiaofeiyang15011 小时前
第六章 · 桥接 — 三支毛笔,画出九种颜色
设计模式
鹿角片ljp11 小时前
KV Cache 解析
java·算法