大白话说Java设计模式-28-模板方法模式(业务实战篇)

📌 PDF :AI人工智能 --- 大模型微调与部署实战项目

大白话说Java设计模式-28-模板方法模式(业务实战篇):大白商城下单骨架的"算法模板"

📌 一句话本质:模板方法就是"父类定骨架,子类填细节",把不变的流程抽到父类,变的细节让子类实现。

🏷️ 标签:模板方法 / Java 设计模式 / 算法骨架 / 下单流程 / 大白商城 🎯 适合:初中级后端 / 经常被"通用流程"困扰的工程师


目录


一、业务场景引入:为什么需要"算法骨架"?

大白商城 2024 年重构订单中心,遇到一个老大难:

"普通下单、秒杀下单、拼团下单流程不一样,但核心步骤都一样(创建订单→减库存→支付→通知),每个实现都把流程复制一遍!"

我打开代码一看:

java 复制代码
/**
 * ❌ 大白商城老的下单代码
 */
@Service
public class OrderServiceV0 {

    public Order createOrder(Goods goods, User user) {
        // 1️⃣ 创建订单
        Order order = new Order();
        order.setUserId(user.getId());
        order.setGoodsId(goods.getId());
        // ... 50 行
        orderDao.save(order);

        // 2️⃣ 减库存
        stockService.decrease(goods.getId(), 1);

        // 3️⃣ 支付
        payService.pay(order);

        // 4️⃣ 通知
        notifyService.notify(order);
        return order;
    }

    public Order seckillOrder(Goods goods, User user) {
        // ❌ 又复制一遍
        Order order = new Order();
        // ... 50 行
        // 2️⃣ 减库存
        stockService.decrease(goods.getId(), 1);
        // 3️⃣ 支付
        payService.pay(order);
        // 4️⃣ 通知
        notifyService.notify(order);
        return order;
    }

    public Order grouponOrder(Goods goods, User user) {
        // ❌ 又复制一遍
        // ...
    }
}

这种代码的痛点

序号 问题 后果
核心流程重复 3 遍 改流程要改 3 处
新人接手难 不知道哪个是核心
业务流程改一处忘一处 Bug 频出

老板要的"通用流程只写一次",怎么搞?

答案就是------模板方法模式

1.1 大白话讲透模板方法

继续打比方:

场景:3 种下单方式(普通/秒杀/拼团),但核心流程都一样(创建订单→减库存→支付→通知)。

  • 错误做法:每个下单方式都把流程复制一遍
  • 正确做法
    1. 父类定骨架AbstractOrderProcessor.processOrder() 把流程固定
    2. 子类填细节NormalOrderProcessor / SeckillOrderProcessor / GrouponOrderProcessor 只实现不同部分
    3. 业务方 只调 processOrder(),不关心是哪种下单

模板方法 = 父类定算法骨架,子类实现具体步骤

1.2 模板方法的 3 个真实场景

大白商城里,模板方法用在:

场景 "父类骨架" "子类差异"
下单流程 4 步固定 普通/秒杀/拼团
数据导入 读取→转换→入库 Excel/CSV/API
报表生成 查询→聚合→导出 日报/月报/年报
支付回调 验签→处理→响应 支付宝/微信/银联
缓存加载 检查→加载→回填 多级缓存

任何"流程固定,细节有变"的场景,都用模板方法


二、反面教材:复制粘贴的"灾难现场"

我们看 4 个反面教材,看它们是怎么一步步崩的。

2.1 反面教材 v1:完全复制粘贴

java 复制代码
/**
 * ❌ 反面教材 v1:完全复制粘贴
 */
public class OrderServiceV1 {
    public Order normalOrder() {
        // 流程 4 步
    }
    public Order seckillOrder() {
        // 复制 4 步
    }
    public Order grouponOrder() {
        // 复制 4 步
    }
}

翻车现场

序号 问题 后果
核心流程重复 3 遍 改一处要改 3 处
新人改漏 Bug 频出

2.2 反面教材 v2:抽公共方法但仍重复调用

java 复制代码
/**
 * ❌ 反面教材 v2:抽公共方法但仍重复调用
 */
public class OrderServiceV2 {
    public Order normalOrder() {
        createOrder();  // 子方法
        decreaseStock();
        pay();
        notify();
        return order;
    }
    public Order seckillOrder() {
        createOrder();
        decreaseStock();
        pay();
        notify();
    }
}

翻车现场

序号 问题 后果
流程顺序无法控制 子类可以乱调
子方法容易漏调 流程不完整

2.3 反面教材 v3:流程控制分散

java 复制代码
/**
 * ❌ 反面教材 v3:流程控制分散
 */
public class OrderServiceV3 {
    public Order order(OrderContext ctx) {
        if (ctx.isNormal()) {
            // 50 行 normal
        } else if (ctx.isSeckill()) {
            // 50 行 seckill
        }
        // 流程控制全在业务方
    }
}

翻车现场

序号 问题 后果
业务方要知道所有细节 复杂
加新流程要改业务方 改老代码

2.4 反面教材 v4:完全没抽象

java 复制代码
/**
 * ❌ 反面教材 v4:完全没抽象
 */
public class OrderServiceV4 {
    public void process() {
        // 1000 行所有逻辑混在一起
    }
}

翻车现场

序号 问题 后果
无法复用 改一处牵动全身
无法测试 单元测试跑不动

2.5 4 个反面教材的共同病根

痛点 反模式方案能不能解决?
通用流程只写一次 ❌ 全部重复
流程顺序控制 ❌ 全部混乱
子类专注细节 ❌ 全部复杂
业务方只调一处 ❌ 全部要懂

必须上模板方法模式


三、模式原理:模板方法的"父类定骨架"

3.1 模板方法的 2 个核心概念

模板方法(Template Method)

  • 定义在父类的final 方法
  • 控制算法骨架(流程)
  • 不允许子类覆盖

钩子方法(Hook Method)

  • 定义在父类的普通方法(可被子类覆盖)
  • 流程中的可变步骤
  • 由子类选择性覆盖

大白商城例子

方法 类型 是否允许覆盖
processOrder() 模板方法(final) ❌ 不允许
createOrder() 钩子方法 ✅ 子类可覆盖
decreaseStock() 钩子方法 ✅ 子类可覆盖
pay() 钩子方法 ✅ 子类可覆盖
notify() 钩子方法 ✅ 子类可覆盖
beforeProcess() 钩子方法 ✅ 子类可覆盖(可选)
afterProcess() 钩子方法 ✅ 子类可覆盖(可选)

3.2 模板方法的 4 个核心角色

角色 职责 例子
抽象模板(Abstract Template) 定义模板方法 + 钩子方法 AbstractOrderProcessor
具体模板 A(Concrete Template A) 实现钩子方法 A NormalOrderProcessor
具体模板 B(Concrete Template B) 实现钩子方法 B SeckillOrderProcessor
客户端(Client) 调用模板方法 OrderService

关键点客户端只调 processOrder(),不知道内部细节

3.3 一张图看懂模板方法

复制代码
AbstractOrderProcessor(抽象模板)
  ├── final processOrder()  ← 模板方法,固定流程
  │     ├── createOrder()    ← 钩子方法
  │     ├── decreaseStock()  ← 钩子方法
  │     ├── pay()            ← 钩子方法
  │     └── notify()         ← 钩子方法
  │
  ├── NormalOrderProcessor   ← 普通下单
  ├── SeckillOrderProcessor  ← 秒杀下单
  └── GrouponOrderProcessor  ← 拼团下单

3.4 模板方法的"灵魂三问"

Q1:模板方法 vs 策略,区别是什么?

答:

  • 模板方法继承 + 父类定骨架(流程固定,细节变)
  • 策略组合 + 算法族独立(算法独立,可切换)
  • 模板方法用继承策略用接口
  • 模板方法"流程被控制",策略"完全自由"

Q2:模板方法 vs 责任链,区别是什么?

答:

  • 模板方法固定流程(4 步按顺序)
  • 责任链链式处理(每一步独立,可中断)
  • 模板方法"流程被父类控制",责任链"每一步决定是否继续"
  • 模板方法用在"流程固定",责任链用在"流程可中断"

Q3:模板方法的钩子方法怎么设计?

答:

  • 必选钩子(abstract):子类必须实现
  • 可选钩子(空实现):子类可选择性覆盖
  • 空实现的好处:子类不用被迫实现所有方法

3.5 模板方法的 3 种实现方式

方式 特点 适用
abstract 钩子 子类必须实现 关键步骤
空实现钩子 子类可选择性覆盖 可选步骤
protected + final 模板 严格控制流程 严格流程

大白商城主推 abstract + 空实现组合


四、实战代码:大白商城下单流程模板完整实现

下面是大白商城生产环境在用的模板实现,全套代码可直接复制到 IDEA 跑

4.1 项目环境与依赖

pom.xml

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
                             https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
        <relativePath/>
    </parent>

    <groupId>com.dabai.mall</groupId>
    <artifactId>mall-design-pattern-28</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <name>mall-design-pattern-28</name>
    <description>大白商城 - 设计模式 28 模板方法</description>

    <properties>
        <java.version>17</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

4.2 抽象模板:AbstractOrderProcessor

java 复制代码
package com.dabai.mall.order.template;

import com.dabai.mall.order.Order;
import com.dabai.mall.order.OrderContext;
import com.dabai.mall.order.OrderResult;
import lombok.extern.slf4j.Slf4j;

/**
 * ✅ 抽象模板:下单流程
 * <p>
 * - 模板方法 processOrder() 固定流程(final)
 * - 钩子方法 createOrder / decreaseStock / pay / notify 由子类实现
 *
 * @author 大白商城技术团队
 */
@Slf4j
public abstract class AbstractOrderProcessor {

    /**
     * ✅ 模板方法:固定流程(final,不允许子类覆盖)
     */
    public final OrderResult processOrder(OrderContext context) {
        log.info("【下单流程】开始, type={}", getOrderType());
        OrderResult result = new OrderResult();

        try {
            // 1️⃣ 钩子:下单前置
            beforeProcess(context);

            // 2️⃣ 钩子:创建订单
            Order order = createOrder(context);
            result.setOrder(order);

            // 3️⃣ 钩子:扣减库存
            decreaseStock(order, context);

            // 4️⃣ 钩子:支付
            pay(order, context);

            // 5️⃣ 钩子:通知
            notify(order, context);

            // 6️⃣ 钩子:下单后置
            afterProcess(context, result);

            result.setSuccess(true);
        } catch (Exception e) {
            log.error("【下单流程】失败", e);
            result.setSuccess(false);
            result.setErrorMessage(e.getMessage());
        }

        log.info("【下单流程】结束, success={}", result.isSuccess());
        return result;
    }

    // ===== 必选钩子:abstract,子类必须实现 =====

    /**
     * ✅ 钩子:创建订单
     */
    protected abstract Order createOrder(OrderContext context);

    /**
     * ✅ 钩子:扣减库存
     */
    protected abstract void decreaseStock(Order order, OrderContext context);

    /**
     * ✅ 钩子:支付
     */
    protected abstract void pay(Order order, OrderContext context);

    /**
     * ✅ 钩子:通知
     */
    protected abstract void notify(Order order, OrderContext context);

    // ===== 可选钩子:空实现,子类可选择性覆盖 =====

    /**
     * 钩子:下单前置
     */
    protected void beforeProcess(OrderContext context) {
        log.debug("【下单】前置检查");
    }

    /**
     * 钩子:下单后置
     */
    protected void afterProcess(OrderContext context, OrderResult result) {
        log.debug("【下单】后置处理");
    }

    /**
     * 钩子:获取下单类型
     */
    protected abstract String getOrderType();
}

配套 DTO

java 复制代码
package com.dabai.mall.order;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;

/**
 * 下单上下文
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderContext {

    private String userId;
    private String goodsId;
    private Integer quantity;
    private BigDecimal amount;
    private String channel;  // 支付渠道
    private String address;
}

/**
 * 订单
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
class Order {

    private String orderId;
    private String userId;
    private String goodsId;
    private Integer quantity;
    private String status;  // 待支付/已支付
}

/**
 * 下单结果
 */
@Data
class OrderResult {

    private boolean success;
    private Order order;
    private String errorMessage;
}

4.3 具体模板:3 种下单

NormalOrderProcessor

java 复制代码
package com.dabai.mall.order.template;

import com.dabai.mall.order.Order;
import com.dabai.mall.order.OrderContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

/**
 * ✅ 具体模板:普通下单
 */
@Slf4j
@Component
public class NormalOrderProcessor extends AbstractOrderProcessor {

    @Override
    protected Order createOrder(OrderContext context) {
        log.info("【普通下单】创建订单, userId={}, goodsId={}",
                context.getUserId(), context.getGoodsId());
        return Order.builder()
                .orderId("NORMAL_" + System.currentTimeMillis())
                .userId(context.getUserId())
                .goodsId(context.getGoodsId())
                .quantity(context.getQuantity())
                .status("PENDING_PAY")
                .build();
    }

    @Override
    protected void decreaseStock(Order order, OrderContext context) {
        log.info("【普通下单】扣减库存, goodsId={}, quantity={}",
                order.getGoodsId(), order.getQuantity());
        // 普通下单:直接扣减
    }

    @Override
    protected void pay(Order order, OrderContext context) {
        log.info("【普通下单】支付, orderId={}, amount={}",
                order.getOrderId(), context.getAmount());
    }

    @Override
    protected void notify(Order order, OrderContext context) {
        log.info("【普通下单】通知, orderId={}", order.getOrderId());
    }

    @Override
    protected String getOrderType() {
        return "NORMAL";
    }
}

SeckillOrderProcessor

java 复制代码
package com.dabai.mall.order.template;

import com.dabai.mall.order.Order;
import com.dabai.mall.order.OrderContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

/**
 * ✅ 具体模板:秒杀下单
 */
@Slf4j
@Component
public class SeckillOrderProcessor extends AbstractOrderProcessor {

    @Override
    protected Order createOrder(OrderContext context) {
        log.info("【秒杀下单】创建订单, userId={}, goodsId={}",
                context.getUserId(), context.getGoodsId());
        return Order.builder()
                .orderId("SECKILL_" + System.currentTimeMillis())
                .userId(context.getUserId())
                .goodsId(context.getGoodsId())
                .quantity(context.getQuantity())
                .status("PENDING_PAY")
                .build();
    }

    @Override
    protected void decreaseStock(Order order, OrderContext context) {
        log.info("【秒杀下单】Redis 预扣库存, goodsId={}, quantity={}",
                order.getGoodsId(), order.getQuantity());
        // ✅ 秒杀特色:Redis 预扣 + 异步同步 MySQL
    }

    @Override
    protected void pay(Order order, OrderContext context) {
        log.info("【秒杀下单】极速支付, orderId={}", order.getOrderId());
    }

    @Override
    protected void notify(Order order, OrderContext context) {
        log.info("【秒杀下单】秒杀成功通知, orderId={}", order.getOrderId());
    }

    @Override
    protected void afterProcess(OrderContext context, com.dabai.mall.order.OrderResult result) {
        // ✅ 秒杀特色:记录秒杀统计
        log.info("【秒杀下单】统计: goodsId={}, success={}",
                context.getGoodsId(), result.isSuccess());
    }

    @Override
    protected String getOrderType() {
        return "SECKILL";
    }
}

GrouponOrderProcessor

java 复制代码
package com.dabai.mall.order.template;

import com.dabai.mall.order.Order;
import com.dabai.mall.order.OrderContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

/**
 * ✅ 具体模板:拼团下单
 */
@Slf4j
@Component
public class GrouponOrderProcessor extends AbstractOrderProcessor {

    @Override
    protected Order createOrder(OrderContext context) {
        log.info("【拼团下单】创建拼团订单, userId={}, goodsId={}",
                context.getUserId(), context.getGoodsId());
        return Order.builder()
                .orderId("GROUPON_" + System.currentTimeMillis())
                .userId(context.getUserId())
                .goodsId(context.getGoodsId())
                .quantity(context.getQuantity())
                .status("PENDING_GROUP")
                .build();
    }

    @Override
    protected void decreaseStock(Order order, OrderContext context) {
        log.info("【拼团下单】预占库存, goodsId={}, quantity={}",
                order.getGoodsId(), order.getQuantity());
        // ✅ 拼团特色:库存预占,成团后扣减
    }

    @Override
    protected void pay(Order order, OrderContext context) {
        log.info("【拼团下单】暂不支付,等待成团, orderId={}", order.getOrderId());
        // ✅ 拼团特色:成团后才扣款
    }

    @Override
    protected void notify(Order order, OrderContext context) {
        log.info("【拼团下单】通知开团, orderId={}", order.getOrderId());
    }

    @Override
    protected String getOrderType() {
        return "GROUPON";
    }
}

4.4 业务调用方

java 复制代码
package com.dabai.mall.order.service;

import com.dabai.mall.order.OrderContext;
import com.dabai.mall.order.OrderResult;
import com.dabai.mall.order.template.AbstractOrderProcessor;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * ✅ 订单服务:模板方法的调用方
 * <p>
 * 业务方只调 processOrder(),不关心内部流程
 *
 * @author 大白商城技术团队
 */
@Slf4j
@Service
public class OrderService {

    /**
     * 类型 → 处理器
     */
    private final Map<String, AbstractOrderProcessor> processorMap = new HashMap<>();

    @Autowired
    private List<AbstractOrderProcessor> processors;

    @PostConstruct
    public void init() {
        for (AbstractOrderProcessor processor : processors) {
            // 通过 getOrderType() 注册
            register(processor);
        }
    }

    private void register(AbstractOrderProcessor processor) {
        // 简化:实际可用 Bean 名 / 自定义注解
        // 这里用反射拿 getOrderType() 的值
        log.info("【OrderService】注册处理器: {}", 
                processor.getClass().getSimpleName());
    }

    /**
     * ✅ 业务入口:业务方只调这里
     */
    public OrderResult createOrder(String type, OrderContext context) {
        AbstractOrderProcessor processor = findProcessor(type);
        // ✅ 调模板方法(不关心是哪种下单)
        return processor.processOrder(context);
    }

    private AbstractOrderProcessor findProcessor(String type) {
        for (AbstractOrderProcessor p : processors) {
            // 实际可用 getOrderType() 匹配
            return p;
        }
        throw new IllegalArgumentException("未知下单类型: " + type);
    }
}

关键点

关键点 解释
业务方只调 processOrder() 不知道 4 步流程
加新下单? 只加 Processor 类
改流程? 只改 AbstractOrderProcessor

4.5 单元测试

java 复制代码
package com.dabai.mall.order.template;

import com.dabai.mall.order.OrderContext;
import com.dabai.mall.order.OrderResult;
import org.junit.jupiter.api.Test;

import java.math.BigDecimal;

import static org.junit.jupiter.api.Assertions.*;

/**
 * 模板方法模式完整单元测试
 */
class AbstractOrderProcessorTest {

    @Test
    void testNormalOrder() {
        AbstractOrderProcessor processor = new NormalOrderProcessor();

        OrderContext context = OrderContext.builder()
                .userId("USER_001")
                .goodsId("GOODS_001")
                .quantity(1)
                .amount(new BigDecimal("100"))
                .channel("alipay")
                .build();

        OrderResult result = processor.processOrder(context);

        assertTrue(result.isSuccess());
        assertNotNull(result.getOrder());
        assertTrue(result.getOrder().getOrderId().startsWith("NORMAL_"));
    }

    @Test
    void testSeckillOrder() {
        AbstractOrderProcessor processor = new SeckillOrderProcessor();

        OrderContext context = OrderContext.builder()
                .userId("USER_002")
                .goodsId("GOODS_002")
                .quantity(1)
                .amount(new BigDecimal("50"))
                .build();

        OrderResult result = processor.processOrder(context);

        assertTrue(result.isSuccess());
        assertTrue(result.getOrder().getOrderId().startsWith("SECKILL_"));
    }

    @Test
    void testGrouponOrder() {
        AbstractOrderProcessor processor = new GrouponOrderProcessor();

        OrderContext context = OrderContext.builder()
                .userId("USER_003")
                .goodsId("GOODS_003")
                .quantity(1)
                .amount(new BigDecimal("80"))
                .build();

        OrderResult result = processor.processOrder(context);

        assertTrue(result.isSuccess());
        assertTrue(result.getOrder().getOrderId().startsWith("GROUPON_"));
    }

    @Test
    void testTemplateMethodFinal() {
        // ✅ 模板方法 processOrder 是 final
        // 子类无法覆盖流程
        AbstractOrderProcessor processor = new NormalOrderProcessor();
        assertEquals("NORMAL", processor.getOrderType());
    }

    @Test
    void testHookOverride() {
        // ✅ SeckillOrderProcessor 覆盖了 afterProcess
        AbstractOrderProcessor seckill = new SeckillOrderProcessor();
        OrderContext context = OrderContext.builder()
                .userId("USER_004")
                .goodsId("GOODS_004")
                .quantity(1)
                .build();
        OrderResult result = seckill.processOrder(context);
        // 验证 afterProcess 跑了(log 里能看到)
        assertTrue(result.isSuccess());
    }
}

五、模板方法 vs 策略 / 责任链

5.1 一张表看清区别

模式 控制方式 实现方式 流程
模板方法 父类控制 继承 固定
策略 客户端控制 组合 自由
责任链 每步控制 链式 链式

5.2 一个具体例子区分 3 个模式

大白商城

模式 例子
模板方法 下单流程(4 步固定)
策略 支付方式(4 种独立)
责任链 风控流程(每步独立)

5.3 决策树

复制代码
有通用流程?
├── 是
│   ├── 流程固定?
│   │   ├── 是 → ✅ 模板方法
│   │   └── 否(可中断)→ ✅ 责任链
└── 否
    ├── 多种算法可切换?
    │   ├── 是 → ✅ 策略
    │   └── 否 → 普通类

5.4 模板方法 vs 策略:易混淆

维度 模板方法 策略
控制方 父类 客户端
实现 继承 接口
流程 固定 自由
改动点 子类填细节 客户端选策略

大白商城选型

场景 选哪个 原因
下单流程 模板方法 流程固定
支付方式 策略 4 种独立
风控规则 责任链 可中断
数据导入 模板方法 流程固定

六、Spring 的 AbstractApplicationContext 怎么用模板方法?

6.1 AbstractApplicationContext.refresh():经典模板方法

Spring 容器启动用模板方法模式

java 复制代码
/**
 * ✅ Spring 模板方法:容器刷新
 */
public abstract class AbstractApplicationContext extends ... {

    /**
     * ✅ 模板方法:固定 12 步
     */
    public void refresh() throws BeansException, IllegalStateException {
        synchronized (this.startupShutdownMonitor) {
            // 1️⃣ 钩子:准备刷新
            prepareRefresh();

            // 2️⃣ 钩子:获取 BeanFactory
            ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

            // 3️⃣ 钩子:准备 BeanFactory
            prepareBeanFactory(beanFactory);

            // 4️⃣ 钩子:后置处理 BeanFactory
            postProcessBeanFactory(beanFactory);

            // 5️⃣ 钩子:调用 BeanFactoryPostProcessor
            invokeBeanFactoryPostProcessors(beanFactory);

            // 6️⃣ 钩子:注册 BeanPostProcessor
            registerBeanPostProcessors(beanFactory);

            // 7️⃣ 钩子:初始化消息源
            initMessageSource();

            // 8️⃣ 钩子:初始化事件广播
            initApplicationEventMulticaster();

            // 9️⃣ 钩子:子类扩展
            onRefresh();

            // 🔟 钩子:注册监听器
            registerListeners();

            // 1️⃣1️⃣ 钩子:实例化所有非懒加载单例
            finishBeanFactoryInitialization(beanFactory);

            // 1️⃣2️⃣ 钩子:发布刷新事件
            finishRefresh();
        }
    }
}

关键点

关键点 解释
refresh() 模板方法(public,未标 final 但实际不允许覆盖
prepareRefresh() 钩子方法
onRefresh() 钩子方法(子类扩展点)
12 步固定流程 模板

6.2 子类扩展:onRefresh()

java 复制代码
/**
 * ✅ Spring 子类:GenericWebApplicationContext 扩展
 */
public class GenericWebApplicationContext extends GenericApplicationContext 
        implements WebApplicationContext {

    @Override
    protected void onRefresh() {
        // ✅ 钩子覆盖:web 容器特殊处理
        super.onRefresh();
        // ... web 特有初始化
    }
}

关键点

关键点 解释
父类 AbstractApplicationContext 定骨架
子类 GenericWebApplicationContext 覆盖 onRefresh()
加 web 特性 只覆盖钩子

6.3 大白商城的"抄作业"

java 复制代码
/**
 * 借鉴 Spring refresh:通用业务流程
 */
public abstract class AbstractBusinessProcess {

    /**
     * ✅ 模板方法
     */
    public final void process(BusinessContext ctx) {
        check(ctx);
        doBusiness(ctx);
        afterBusiness(ctx);
    }

    protected abstract void check(BusinessContext ctx);
    protected abstract void doBusiness(BusinessContext ctx);

    protected void afterBusiness(BusinessContext ctx) {
        // 钩子:子类可覆盖
    }
}

七、模板方法 + 策略怎么组合?

7.1 组合 1:模板方法内用策略

java 复制代码
/**
 * ✅ 模板方法内用策略:支付步骤用策略
 */
public abstract class AbstractOrderProcessor {

    @Autowired
    private PaymentContext paymentContext;  // 策略上下文

    public final OrderResult processOrder(OrderContext context) {
        // ...
        // 4️⃣ 支付:用策略
        paymentContext.pay(context.getChannel(), ...);
        // ...
    }
}

7.2 组合 2:策略 + 模板方法 = 策略骨架

java 复制代码
/**
 * ✅ 策略抽象类(带模板)
 */
public abstract class AbstractPaymentStrategy implements PaymentStrategy {

    public final PaymentResult doPay(PaymentRequest request) {
        // ✅ 模板方法
        validate(request);
        doPayInternal(request);
        recordLog(request);
        return result;
    }

    protected abstract void doPayInternal(PaymentRequest request);

    protected void validate(PaymentRequest request) {
        // 公共校验
    }

    protected void recordLog(PaymentRequest request) {
        // 公共日志
    }
}

@Component
public class AlipayStrategy extends AbstractPaymentStrategy {
    @Override
    protected void doPayInternal(PaymentRequest request) {
        // 支付宝特有支付
    }
}

7.3 组合 3:钩子方法 + 工厂方法

java 复制代码
/**
 * ✅ 钩子 + 工厂
 */
public abstract class AbstractOrderProcessor {

    /**
     * 钩子:获取支付策略(子类可覆盖)
     */
    protected PaymentStrategy getPaymentStrategy(String channel) {
        return PaymentStrategyFactory.getStrategy(channel);
    }
}

八、工程决策 Checklist

8.1 ✅ 这 5 种情况,强烈建议用模板方法

序号 场景 原因
流程固定 下单/数据导入/报表
多个变体 普通/秒杀/拼团
流程被严格控制 不允许子类乱改
复用公共逻辑 父类抽公共
子类只填差异 关注点分离

8.2 ❌ 这 5 种情况,绝对不要用模板方法

序号 场景 原因
流程不固定 用策略 / 责任链
只有一个变体 没意义
算法之间独立 用策略
业务简单 过度设计
需要运行时切换 用策略

8.3 ⚠️ 模板方法的 6 大常见坑

序号 表现 解决方案
模板方法被覆盖 父类失去控制 标 final
钩子方法太多 子类被迫实现 abstract + 空实现
继承层次深 难调试 控制继承层数
父类臃肿 难维护 父类只管骨架
子类破坏流程 跳过关键步骤 模板 final
钩子语义不清 命名混乱 命名规范

8.4 面试官视角:模板方法高频追问

Q1:模板方法 vs 策略,区别是什么?

答:模板方法用继承 (父类定骨架),策略用组合 (算法族独立)。模板方法"流程被控制",策略"完全自由"

Q2:钩子方法 vs 抽象方法,区别是什么?

答:抽象方法子类必须实现 ,钩子方法子类可选择性覆盖钩子给子类扩展点,但不强求

Q3:Spring refresh() 是模板方法吗?

答:是的AbstractApplicationContext.refresh() 定 12 步骨架,onRefresh() 等钩子让子类扩展。WebApplicationContext 覆盖 onRefresh() 加入 web 特性


九、与其他模式协作

9.1 模板方法 + 策略 = 流程固定 + 算法可切换

9.2 模板方法 + 工厂方法 = 钩子内创建对象

java 复制代码
protected Order createOrder(OrderContext context) {
    Order order = OrderFactory.createOrder(context);
    return order;
}

9.3 模板方法 + 装饰器 = 钩子被装饰

java 复制代码
public abstract class AbstractOrderProcessor {
    protected abstract void notify(Order order, OrderContext context);
}

// 装饰:加日志
public class LoggedNotifyOrderProcessor extends AbstractOrderProcessor {
    // ...
    @Override
    protected void notify(Order order, OrderContext context) {
        log.info("通知: orderId={}", order.getOrderId());
        // ... 调原 notify
    }
}

9.4 大白商城模式协作全景图

复制代码
                    ┌──────────────┐
                    │  模板方法    │ ← 本篇
                    └──────┬───────┘
                           │
       ┌───────────┬───────┼───────┬───────────┐
       │           │       │       │           │
   ┌───▼───┐  ┌────▼───┐ ┌▼────┐ ┌▼─────┐  ┌───▼────┐
   │策略   │  │工厂方法│ │装饰器│ │单例  │  │ 责任链 │
   │(步骤) │  │(创建)  │ │(增强)│ │(骨架)│  │ (变体) │
   └───────┘  └────────┘ └──────┘ └──────┘  └───────┘
   26 篇       04 篇     16 篇     02 篇     46 篇

十、本篇小结 + 下篇预告

10.1 本篇小结(5 个核心要点)

  1. 本质 :模板方法 = 父类定骨架 + 子类填细节加新变体只加 1 个类
  2. 场景 :下单流程 / 数据导入 / 报表生成,流程固定,细节有变
  3. 对比:模板方法 vs 策略(继承 vs 组合)/ 模板方法 vs 责任链(固定 vs 链式)。
  4. Spring refresh() :经典模板方法,12 步骨架 + 钩子扩展
  5. 避坑:模板方法被覆盖 / 钩子过多 / 继承层次深 / 父类臃肿。

10.2 一句话总结

模板方法不是"换种方式继承",是"用父类控制流程" 。大白商城普通/秒杀/拼团 3 种下单,核心 4 步只写一次 ,加新下单只加 1 个 Processor。改流程?只改父类

10.3 知识脑图

复制代码
模板方法模式
├── 2 个核心概念
│   ├── 模板方法(final)
│   └── 钩子方法(abstract + 空实现)
├── 4 大角色
│   ├── 抽象模板(AbstractOrderProcessor)
│   ├── 具体模板 A(NormalOrderProcessor)
│   ├── 具体模板 B(SeckillOrderProcessor)
│   └── 客户端(OrderService)
├── 3 个具体模板
│   ├── NormalOrderProcessor(普通下单)
│   ├── SeckillOrderProcessor(秒杀下单)
│   └── GrouponOrderProcessor(拼团下单)
├── 模式对比
│   ├── vs 策略(继承 vs 组合)
│   ├── vs 责任链(固定 vs 链式)
│   └── vs 状态(流程 vs 状态机)
├── Spring refresh()
│   ├── 12 步骨架
│   ├── onRefresh() 钩子
│   └── WebApplicationContext 扩展
└── 模式协作
    ├── + 策略(流程内用策略)
    ├── + 工厂(钩子创建对象)
    └── + 装饰器(钩子被装饰)

10.4 下篇预告

第 29 篇【模板方法模式 - 源码剖析篇】:JDK / Spring 中的模板实现

下一篇我们会深入源码,回答三个问题:

  1. JDK 的 AbstractList / AbstractMap 怎么用模板方法?
  2. Spring 的 AbstractApplicationContext.refresh() 怎么用模板?
  3. JdbcTemplate / RestTemplate 怎么用模板简化?

并附完整的源码解读 + 流程图 + 大白商城的"抄作业"实践。


觉得对您有帮助,麻烦 点点关注啦 ,您的关注是我创作的最大动力~ 🎯

相关推荐
奥莱维1 小时前
KNX酒店方案_KNX专用线与高端酒店技术逻辑
java·服务器·前端·数据库
工业一体机老司机1 小时前
Linux工业一体机自启动服务配置:systemd服务单元编写与开机优化实战
java·linux·服务器
weixin_538601972 小时前
智能体测开Day56
java
杨丰玮4182 小时前
从零手写Java飞机躲障碍游戏|Swing绘图、鼠标跟随、计时器碰撞检测实战(五)
java·python·游戏·游戏引擎·图形渲染·动画·贴图
莫得感情 o2 小时前
踩坑 - 压测三轮后 502:一个默认 10 的连接池如何拖垮整个服务
java
Escalating_xu2 小时前
【Linux线程】线程控制全解析:终止、join/detach、cancel、线程栈与 NPTL(下篇)
java·linux·运维
2602_959960922 小时前
电商大厂Java面试实录:Spring Boot/JVM/Redis/Kafka/微服务/安全/测试全解析
java·jvm·spring boot·redis·面试题
吃饱了得干活3 小时前
限界上下文之后:微服务怎么拆、上下文怎么聊?
java·后端·架构
yngsqq3 小时前
窗体快速取消
java·开发语言