主题
界面
主题
《设计模式:结构型》讲"对象怎么组合";本节讲**"行为怎么组织"**:
行为型模式都在解决同一件事:把「会变的那部分行为」从流程里摘出来 ✓✓★ 本节用真实的定价场景,量出"if-else 改 1 处 vs 策略模式改 0 处"的差别 ✓
final ★★;Map 与函数式接口吗?(《Lambda 表达式》)✓先看一段"改不动的定价代码" ✗:
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("未知等级");
}★ 它有三个问题 ✓:
| 问题 | 说明 |
|---|---|
| 每加一个等级都要改它 ✗ | 违反开闭原则 |
| 每个折扣规则无法单独测试 ✗✗ | 只能"传不同参数看结果" |
| 规则一多就嵌套 ✗ | 如果再加"活动折扣""限时折扣",组合起来就是灾难 |
✓ 改造后:
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 分支" ✗
★ 进阶:策略 + 注册表 + 配置 ✓✓:
// 真实项目常把策略从配置/数据库/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 ✓ | 自动配置的加载机制 |
★ 它带来的运维能力 ✓✓:改规则不用重新发版 ✓
场景:下单成功后要"加积分、发通知、写埋点" ✗ —— 如果直接调用,createOrder 会认识所有下游 ✗✗
✓ 观察者:
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 ✓✓ |
| ② | 单个监听器异常被隔离 ★★★ | 少了它,一个埋点系统挂了会导致下单失败 ✗✗ |
★ 实测里故意放了一个"总是抛异常"的监听器 ✓ —— 验证了"它失败不影响主流程与其他监听器" ✓✓
★ 真实演进路径(同一个思想的不同实现) ✓✓:
方法内直接调用 → 观察者/监听器 → Spring 的 ApplicationEvent → 消息队列 ✓★ 最后一步的收益 ✓:跨进程、可重放、削峰填谷(《消息队列基础与选型》 会讲)✓
实测代码的结构 ✓:
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(); }
}★ 实测输出 ✓:
CSV 导入:共 3 行,有效 2 行,写入 2 行
JSON 导入:共 3 行,有效 2 行,写入 2 行★ 模板方法的价值 ✓✓:
流程只有一份,子类只填变化点 ✓
如果把"解析→校验→入库→报告"复制到每个 Importer 里,将来想加一步"去重"就要改 N 处 ✗
★ 为什么流程方法要 final ✓✓:
它保证"流程不会被某个子类偷偷改掉" ✓
这正是模板方法的意义:流程是框架的,变化点才是你的 ✓
★ 框架里的对应 ✓:
| 框架 | 固定流程 | 变化点 |
|---|---|---|
JdbcTemplate ★★ | 连接获取 / 异常转换 / 资源释放 | 你写 SQL 与行映射 ✓ |
refresh()(Spring) | Bean 生命周期大流程 | 后置处理器 ✓ |
HttpServlet.service() | 按方法分发 | doGet / doPost ✓ |
★ 与策略模式的区别(常考) ✓✓:
| 模板方法 | 策略 | |
|---|---|---|
| 实现方式 | 继承(子类填步骤) | 组合(整个算法替换) |
| 变化粒度 | 流程中的某几步 | 整个算法 |
| 确定时机 | 编译期 | 运行期 ✓ |
结构:每个处理器决定"处理"还是"传给下一个" ✓
★ 真实场景 ✓:
| 场景 | 处理器链 |
|---|---|
| Web 过滤器 ★★ | 鉴权 → 限流 → 日志 → 业务 |
| 审批流程 ★ | 组长 → 经理 → 总监(金额决定走到哪一级) |
| 网关拦截 ★ | 签名校验 → 参数校验 → 路由 |
★ 与本项目已有内容的呼应 ✓: 本模块在 《JSON 与 XML》 讲过 Filter 链 —— 它就是一个责任链,且顺序由注册顺序决定 ✓✓
| 模式 | 一句话 | 典型 |
|---|---|---|
| 策略 ★★★ | 算法可替换 | 定价、支付、排序 |
| 观察者 ★★★ | 状态变化通知多方 | 事件、监听器 |
| 模板方法 ★★★ | 流程固定、步骤可变 | JdbcTemplate |
| 责任链 ★★ | 请求在链上传递 | Filter、审批 |
| 命令 ★ | 把请求封装成对象 | 任务队列、撤销 |
| 状态 ★ | 状态决定行为 | 订单状态机 |
| 迭代器 | 统一遍历 | Iterator |
interface Discount { BigDecimal apply(BigDecimal amount); }
Map<String, Discount> rules = Map.of(
"NORMAL", a -> a,
"GOLD", a -> a.multiply(new BigDecimal("0.90")));见 5.2 ✓
public String createOrder(...) {
...
pointsService.add(...); // ✗ 业务认识所有下游
smsService.notify(...); // ✗ 加一个下游就改这里
analytics.track(...); // ✗ 埋点挂了会影响下单
}代码:labs/design-patterns/src/main/java/cn/kb/learn/dp/StrategyObserverLesson.java
运行:
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 的计算结果完全一致,差别在"改得动"与"可单测" ✓✓ |
| ② | 观察者让业务零改动地接入新下游,且单点异常被隔离 ✓✓ |
| ③ | 模板方法把流程锁在父类,子类只填变化点 ✓ |
final 去掉,让子类覆盖流程:观察"流程被改坏"的后果 ✓;| # | 错误 | 说明 |
|---|---|---|
| 1 | 策略里写了业务上下文 ✗ | 策略应尽量无状态、可独立测试 ✓ |
| 2 | 观察者里不隔离异常 ✗✗ | 一个下游挂了拖垮主流程 ✓✓ |
| 3 | 观察者里做耗时操作 ✗ | 会拖慢主流程 → 该改成异步或消息队列 ✓ |
| 4 | 模板方法的流程方法没设 final ✗ | 子类可能改坏流程 ✓ |
| 5 | 责任链没有终止条件 ✗✗ | 可能死循环 ✓ |
| 6 | 把策略模式用在"只有两个分支、且不会变"的地方 ✗ | 过度设计 ✓ |
目标:亲手把一段 if-else 改成"可配置的策略表"。
步骤:
StrategyObserverLesson,确认结果一致性与改动点对比 ✓;验收方式:能说出"你的那段 if-else 改成策略后,加新类型要改几处"。
扩展任务:实现一个"可插拔的订单校验链":
final?public void afterOrderCreated(Order order) {
pointsService.add(order);
smsService.send(order);
emailService.send(order);
analytics.track(order);
}这段代码有什么问题?怎么改? 5. (设计)给"订单状态流转"设计实现:待支付 → 已支付 → 已发货 → 已完成,每个状态允许的操作不同,且便于加新状态。
final ★★;《设计模式在 Spring 中的应用》:把学到的模式映射到你每天都在用的框架里。