Skip to content

设计模式:行为型 ​

S4-005建议学高级约 90 分钟JDK 21企业开发进阶 · 软件工程与设计模式

1. 本节定位 ​

《设计模式:结构型》讲"对象怎么组合";本节讲**"行为怎么组织"**:

text
行为型模式都在解决同一件事:把「会变的那部分行为」从流程里摘出来  ✓✓

★ 本节用真实的定价场景,量出"if-else 改 1 处 vs 策略模式改 0 处"的差别 ✓

2. 学习目标 ​

  • 能说出策略模式与模板方法的区别(一个用组合、一个用继承)★★★;
  • 会用注册表让策略"可配置化" ★★;
  • 知道观察者的两个关键设计(业务不认监听器、单点异常要隔离)★★★;
  • 知道模板方法为什么要把流程方法设成 final ★★;
  • 知道责任链的适用场景 ★★。

3. 前置知识检查 ​

4. 为什么需要这个知识 ​

先看一段"改不动的定价代码" ✗:

java
public BigDecimal price(BigDecimal amount, MemberLevel level) {
    if (level == NORMAL)       return amount;
    else if (level == SILVER)  return amount.multiply(new BigDecimal("0.95"));
    else if (level == GOLD)    return amount.multiply(new BigDecimal("0.90"));
    else if (level == DIAMOND) return amount.multiply(new BigDecimal("0.80"));
    throw new IllegalArgumentException("未知等级");
}

★ 它有三个问题 ✓:

问题说明
每加一个等级都要改它 ✗违反开闭原则
每个折扣规则无法单独测试 ✗✗只能"传不同参数看结果"
规则一多就嵌套 ✗如果再加"活动折扣""限时折扣",组合起来就是灾难

5. 核心概念 ​

5.1 策略:把"算法"变成可替换的对象 ★★★ ​

✓ 改造后:

java
private static final Map<MemberLevel, PricingStrategy> STRATEGIES = new EnumMap<>(MemberLevel.class);
static {
    STRATEGIES.put(NORMAL, amount -> amount);
    STRATEGIES.put(GOLD, amount -> amount.multiply(new BigDecimal("0.90")));
}

public BigDecimal price(BigDecimal amount, MemberLevel level) {
    PricingStrategy strategy = STRATEGIES.get(level);
    if (strategy == null) { throw new IllegalArgumentException("未知等级:" + level); }
    return strategy.price(amount);
}

★ 实测对比 ✓:

观察结果
计算结果两种实现完全一致 ✓(差别不在"算得对",而在"改得动")
加一个等级if-else 改 1 处 ✗ / 策略加 1 个实现 ✓
改某等级折扣if-else 改 1 处 ✗ / 策略改 1 个策略 ✓
按等级做单元测试if-else 只能测整个方法 ✗✗ / 策略可单独测每个规则 ✓✓

★★ 第四行是最容易被忽略的收益 ✓✓:

策略模式让"每个折扣规则"可以独立测试 ✓

而在 if-else 里,你很难"只测 GOLD 分支" ✗

★ 进阶:策略 + 注册表 + 配置 ✓✓:

java
// 真实项目常把策略从配置/数据库/SPI 加载,而不是写死在编译期
for (String className : config.getStrategyList()) {
    PricingStrategy s = (PricingStrategy) Class.forName(className).newInstance();
    registry.put(s.name(), s);
}

★ 这就是 SPI(Service Provider Interface)的思路 ✓:

用 SPI 的地方说明
JDBC 驱动 ✓各数据库自己提供实现
SLF4J 日志实现 ✓Logback / Log4j 二选一
Spring 的 spring.factories ✓自动配置的加载机制

★ 它带来的运维能力 ✓✓:改规则不用重新发版 ✓

5.2 观察者:业务与副作用解耦 ★★★ ​

场景:下单成功后要"加积分、发通知、写埋点" ✗ —— 如果直接调用,createOrder 会认识所有下游 ✗✗

✓ 观察者:

java
publisher.subscribe((orderNo, amount) -> pointsService.add(orderNo, amount));
publisher.subscribe((orderNo, amount) -> smsService.notify(orderNo));

String createOrder(String orderNo, BigDecimal amount) {
    String result = "订单 " + orderNo + " 创建成功";        // 核心业务
    for (OrderListener listener : listeners) {
        try { listener.onCreated(orderNo, amount); }
        catch (Exception e) { log.warn("监听器失败", e); }   // ★ 隔离异常
    }
    return result;
}

★ 两个关键设计(本节的实测体现了) ✓✓:

#设计为什么重要
①业务代码不认识任何监听器加积分、发通知、埋点都不改 createOrder ✓✓
②单个监听器异常被隔离 ★★★少了它,一个埋点系统挂了会导致下单失败 ✗✗

★ 实测里故意放了一个"总是抛异常"的监听器 ✓ —— 验证了"它失败不影响主流程与其他监听器" ✓✓

★ 真实演进路径(同一个思想的不同实现) ✓✓:

text
方法内直接调用 → 观察者/监听器 → Spring 的 ApplicationEvent → 消息队列  ✓

★ 最后一步的收益 ✓:跨进程、可重放、削峰填谷(《消息队列基础与选型》 会讲)✓

5.3 模板方法:把流程固定下来 ★★ ​

实测代码的结构 ✓:

java
abstract class DataImporter {
    final String importFrom(String source) {       // ★ final:子类不能改流程
        List<String> lines = parse(source);        // 变化点 1
        List<String> valid = validate(lines);      // 变化点 2
        int inserted = save(valid);                // 变化点 3(有默认实现)
        return report(lines.size(), valid.size(), inserted);   // 固定环节
    }
    protected abstract List<String> parse(String source);
    protected abstract List<String> validate(List<String> lines);
    protected int save(List<String> lines) { return lines.size(); }
}

★ 实测输出 ✓:

text
  CSV 导入:共 3 行,有效 2 行,写入 2 行
  JSON 导入:共 3 行,有效 2 行,写入 2 行

★ 模板方法的价值 ✓✓:

流程只有一份,子类只填变化点 ✓

如果把"解析→校验→入库→报告"复制到每个 Importer 里,将来想加一步"去重"就要改 N 处 ✗

★ 为什么流程方法要 final ✓✓:

它保证"流程不会被某个子类偷偷改掉" ✓

这正是模板方法的意义:流程是框架的,变化点才是你的 ✓

★ 框架里的对应 ✓:

框架固定流程变化点
JdbcTemplate ★★连接获取 / 异常转换 / 资源释放你写 SQL 与行映射 ✓
refresh()(Spring)Bean 生命周期大流程后置处理器 ✓
HttpServlet.service()按方法分发doGet / doPost ✓

★ 与策略模式的区别(常考) ✓✓:

模板方法策略
实现方式继承(子类填步骤)组合(整个算法替换)
变化粒度流程中的某几步整个算法
确定时机编译期运行期 ✓

5.4 责任链:请求在一串处理器里传递 ★★ ​

结构:每个处理器决定"处理"还是"传给下一个" ✓

★ 真实场景 ✓:

场景处理器链
Web 过滤器 ★★鉴权 → 限流 → 日志 → 业务
审批流程 ★组长 → 经理 → 总监(金额决定走到哪一级)
网关拦截 ★签名校验 → 参数校验 → 路由

★ 与本项目已有内容的呼应 ✓: 本模块在 《JSON 与 XML》 讲过 Filter 链 —— 它就是一个责任链,且顺序由注册顺序决定 ✓✓

5.5 行为型模式速览 ★ ​

模式一句话典型
策略 ★★★算法可替换定价、支付、排序
观察者 ★★★状态变化通知多方事件、监听器
模板方法 ★★★流程固定、步骤可变JdbcTemplate
责任链 ★★请求在链上传递Filter、审批
命令 ★把请求封装成对象任务队列、撤销
状态 ★状态决定行为订单状态机
迭代器统一遍历Iterator

6. 递进示例 ​

6.1 最小示例:策略 ​

java
interface Discount { BigDecimal apply(BigDecimal amount); }
Map<String, Discount> rules = Map.of(
        "NORMAL", a -> a,
        "GOLD",   a -> a.multiply(new BigDecimal("0.90")));

6.2 业务示例:观察者 + 异常隔离 ​

见 5.2 ✓

6.3 反例:把观察者写成"直接调用" ​

java
public String createOrder(...) {
    ...
    pointsService.add(...);       // ✗ 业务认识所有下游
    smsService.notify(...);       // ✗ 加一个下游就改这里
    analytics.track(...);         // ✗ 埋点挂了会影响下单
}

7. 完整可运行代码与实测 ​

代码:labs/design-patterns/src/main/java/cn/kb/learn/dp/StrategyObserverLesson.java

运行:

powershell
cd labs
.\mvnw.cmd -pl design-patterns compile
java "-Dstdout.encoding=UTF-8" -cp design-patterns/target/classes cn.kb.learn.dp.StrategyObserverLesson

实测结论 ✓:

#结论
①策略与 if-else 的计算结果完全一致,差别在"改得动"与"可单测" ✓✓
②观察者让业务零改动地接入新下游,且单点异常被隔离 ✓✓
③模板方法把流程锁在父类,子类只填变化点 ✓

8. Debug 观察点 ​

  1. 把观察者里那个抛异常的监听器放在中间:观察后面的监听器是否仍被执行 ★★;
  2. 把模板方法的 final 去掉,让子类覆盖流程:观察"流程被改坏"的后果 ✓;
  3. 把策略换成 if-else 并加第 5 个等级:数一数改了哪个方法 ✓;
  4. 在策略里加一个"抛异常的实现":观察调用方的错误处理 ✓。

9. 常见错误 ​

#错误说明
1策略里写了业务上下文 ✗策略应尽量无状态、可独立测试 ✓
2观察者里不隔离异常 ✗✗一个下游挂了拖垮主流程 ✓✓
3观察者里做耗时操作 ✗会拖慢主流程 → 该改成异步或消息队列 ✓
4模板方法的流程方法没设 final ✗子类可能改坏流程 ✓
5责任链没有终止条件 ✗✗可能死循环 ✓
6把策略模式用在"只有两个分支、且不会变"的地方 ✗过度设计 ✓

10. 动手实验 ​

目标:亲手把一段 if-else 改成"可配置的策略表"。

步骤:

  1. 跑 StrategyObserverLesson,确认结果一致性与改动点对比 ✓;
  2. 给定价加第 5 个等级,用 if-else 加一次,再用策略加一次,对比改动量 ★★;
  3. 给每个等级策略补一个单元测试,体会"可独立测试" ✓✓;
  4. 在观察者链里插入一个"失败"的监听器,验证隔离 ✓;
  5. 把观察者的耗时操作改成异步(丢进线程池),对比主流程耗时 ★。

验收方式:能说出"你的那段 if-else 改成策略后,加新类型要改几处"。

扩展任务:实现一个"可插拔的订单校验链":

  • 校验项可配置(实名、地址、库存、风控)✓;
  • 每个校验项能单独失败并返回原因 ✓;
  • 用责任链或策略 + 列表实现 ✓;
  • 说明:为什么"校验项的顺序"很重要(提示:快的、能早否决的放前面)★★

11. 练习题 ​

  1. 判断:策略模式与模板方法都是"把变化点摘出来",实现手段相同。(对 / 错)
  2. 填空:策略用 ______,模板方法用 ______;观察者的两个关键设计是 ______ 与 ______。
  3. 简答:为什么模板方法的流程方法要设成 final?
  4. (代码阅读)
java
public void afterOrderCreated(Order order) {
    pointsService.add(order);
    smsService.send(order);
    emailService.send(order);
    analytics.track(order);
}

这段代码有什么问题?怎么改? 5. (设计)给"订单状态流转"设计实现:待支付 → 已支付 → 已发货 → 已完成,每个状态允许的操作不同,且便于加新状态。

参考答案(要点)
  1. 错。策略用组合(运行期替换),模板方法用继承(编译期确定) ✓★
  2. 组合;继承;业务不认监听器;单点异常隔离 ✓
  3. 保证流程不被子类改坏 ✓ ——流程属于框架,变化点才属于子类 ✓★
  4. ① 业务方法认识所有下游(每加一个下游就改这里)✗; ② 任一服务抛异常会导致下单失败 ✗✗; 改法:改成事件发布 + 监听器(每个监听器独立 try-catch)✓,耗时操作异步化 ✓
  5. 要点:用状态模式或状态机(每个状态一个实现类,内部决定允许的操作并驱动流转)✓

12. 掌握标准 ​

  • [ ] 能说出策略与模板方法的区别 ★★★;
  • [ ] 会用注册表把策略配置化 ★★;
  • [ ] 知道观察者的两个关键设计,并知道为什么要隔离异常 ★★★;
  • [ ] 知道模板方法为什么要 final ★★;
  • [ ] 知道责任链的典型场景 ★★;
  • [ ] 不做过度设计(两个分支不用策略)✓。

13. 扩展资源 ​

14. 下一步学习 ​

《设计模式在 Spring 中的应用》:把学到的模式映射到你每天都在用的框架里。

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引