她说..头像
关注
常见设计模式-模板方法模式封面图

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

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


一、模板方法模式概述

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

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

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


二、模式结构

2.1 角色说明

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

2.2 基本方法的三种类型

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

2.3 结构伪代码

// 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()

// 【抽象类】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 没有通过继承来扩展,而是通过回调接口让调用方定义差异化的步骤:

// 查询操作 —— 调用方实现【抽象方法】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 支付流程(业务实战案例)

问题背景

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

模板方法(算法骨架)
// 【抽象类】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() = 钩子方法(空方法,子类可选择性重写)
具体子类实现
// 【具体子类】微信支付
@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);
        }
    }
}
调用入口
@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支付流程
抽象类JdbcTemplateAbstractPayService
模板方法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)
  ↓
具体方法:父类实现,子类继承
  ↓
抽象方法:子类必须实现
  ↓
钩子方法:子类可选择性重写

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

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/qq_59093178/article/details/166011974

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--