策略者模式设计思想
# 14.策略者模式设计思想
📚 本篇渐进学习节奏(建议按顺序食用)
- 第 01 节 · 案例引入 — 周日 03:14 风控系统 if-else 30 个分支,新增"东南亚规则"被压在末尾,黑产 6 小时刷走 670 万的 P0 资损事故
- 第 02 节 · 3 次失败探索 — 继承重写 / Map 工厂 / 反射加载,三种直觉方案为何全部翻车
- 第 03 节 · 模式基础 — 从失败清单逆推设计约束 → 标准骨架 Context/Strategy/ConcreteStrategy → 场景识别
- 第 04 节 · 4 种实现对比 — 手写 Map 查表 → 工厂 + 缓存 → Spring 自动注入 → 枚举绑定
- 第 05 节 · 效果对比 — 用前用后 12 维数据说话(事故现场 vs 策略模式重构)
- 第 06 节 · 反面踩坑 — 6 种翻车姿势实录 + 14 个开源案例 + 替代方案汇总
- 第 07 节 · 决策树 — 该不该用 → 用哪种实现 → 速查清单,贴工位上就能用
- 第 08 节 · 总结延伸 — 思考模型沉淀 + 与状态/模板方法/工厂/责任链的精准切割 + 3 道自测题
阅读到任一节卡壳,直接跳回上一节复盘场景;本篇所有代码均可直接运行。
# 目录介绍
# 推荐一个好玩网站
一个最纯粹的技术分享网站,打造精品技术编程专栏!编程进阶网 (opens new window)
https://yccoding.com/
关于设计模式,所有的代码都放到了该项目。设计模式大全 (opens new window)
# 01.案例引入与思考
本篇主线:同一件事的多种"做法"被硬编码到一个方法里——引入策略后,调用方只负责"选一个",不负责"怎么做"。
# 1.1 痛点现场
🔥 模拟事故复盘 · 周日 03:14 · "新增风控规则被压在 if-else 末尾,黑产 6 小时刷走 670 万"
周日凌晨 03:14,某支付平台风控告警群被打爆——"东南亚地区交易笔数异常飙升 4200%"。值班同学打开监控盘一看,过去 6 小时有 14 万笔小额交易(单笔 50-200 元)从印尼/泰国/越南 IP 涌入,全部成功支付。等运营介入时已经造成:
- 直接资损:670 万人民币(黑产用盗刷的信用卡疯狂购买虚拟商品后转卖);
- 拒付索赔:信用卡组织 chargeback 罚金 + 赔付预估 230 万;
- 合规告警:境外反洗钱监管被动,6 个月观察期;
- 平台等级下调:从 VISA Tier 1 降为 Tier 2,结算费率全面上浮 0.3%。
周一复盘会上,代码翻出来——风控决策的核心方法
RiskEngine.judge()是这样的:public RiskResult judge(Transaction tx) { // 30+ 条 if-else,从 2018 年起每个新需求都加一行 if (tx.getAmount().compareTo(BIG_AMOUNT) > 0) { ... return BLOCK; } // 大额 else if (tx.getCard().isBlackList()) { ... return BLOCK; } // 黑名单 else if (tx.getDeviceFingerprint().riskScore() > 80) { ... return BLOCK; } // 设备风险 else if (tx.getUser().is3DSExempt() && tx.getAmount() > 500) { ... return REVIEW; } // 3DS 豁免 // ...... 中间 25 条规则 ...... // ✨ 新增第 30 条:东南亚规则(小王 2 周前提交) else if (tx.getCountry().matches("ID|TH|VN|PH|MY") && tx.getAmount().compareTo(BigDecimal.valueOf(50)) > 0 && tx.getCard().getIssuer().contains("CREDIT")) { return REVIEW; } return PASS; // ❌ 兜底放行 }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15根因有 3 个,一个比一个炸裂:
- 顺序错位:第 30 条规则被压在最末尾,前面"3DS 豁免规则"对所有国家都适用,且
amount > 500才拦截,黑产精准选择 50-200 元绕过;- 规则交叉漏判:黑产研究了公开 SDK 文档,发现东南亚 + 借记卡是漏洞,改用借记卡(不匹配
CREDIT)发起 49.9 元交易,完美绕过新规则;- 改一行牵全身:小王上线前对前 29 条规则不敢动,只能"在末尾追加",但 if-else 的语义恰恰是"前面命中后面就跳过"——新规则永远是最弱的。
复盘结论不是"小王规则没写好"——而是 30 条风控规则的"判定逻辑"被硬编码在同一个方法里,任何新规则的命中都依赖前面 29 条规则的执行顺序,这种"改一行牵全身"的代码本质上不可维护。
# 1.2 直觉实现复现
你也能写出这种代码。 一个新人接手电商系统,要"根据促销类型算价格",第一反应往往是这样:
public BigDecimal calcPrice(Order o) {
if ("FULL_REDUCE".equals(o.getPromoType())) {
return o.getAmount().subtract(new BigDecimal("30")); // 满200减30
} else if ("DISCOUNT".equals(o.getPromoType())) {
return o.getAmount().multiply(new BigDecimal("0.8")); // 打8折
} else if ("FREE_SHIPPING".equals(o.getPromoType())) {
return o.getAmount(); // 免邮费
} else if ("LADDER_REDUCE".equals(o.getPromoType())) {
// 阶梯满减 满300减50 满500减100
return o.getAmount().subtract(calcLadderOff(o.getAmount()));
} else if ("SECKILL".equals(o.getPromoType())) {
return o.getAmount().multiply(new BigDecimal("0.5")); // 秒杀5折
} else if ("PIN".equals(o.getPromoType())) {
return o.getAmount().multiply(new BigDecimal("0.7")); // 拼团7折
}
// ... 半年后 30 种活动,方法膨胀到 500 行
return o.getAmount();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
🧪 跑一下,亲眼看到 bug
// 1. 产品:加一个"跨店满减"活动 → 在 calcPrice 末尾加 else-if
// 2. 产品:加一个"新客专享"活动 → 又在 calcPrice 末尾加 else-if
// 3. 产品:活动叠加 "满减 + 新客券" → 还要在外层再加一层判断
// 4. A/B 测试:"满减"灰度 50% 用户 → 改代码加 if-else + 配置,然后重发版
// 5. 三个月后: calcPrice 500 行,每次改都要跑 500 行全量回归测试
// 更致命的是: 购物车预览、下单计算、订单详情都要算价格
// 同一套 if-else 复制粘贴到三个地方 → 满减规则改了,3 个地方都要改 → 漏了 1 个 → 前端展示 200,点下单扣款 230
2
3
4
5
6
7
8
事故现场重现完毕——"多种算法的选择逻辑"和"每种算法的具体实现"被焊死在同一个方法里,新加一种做法就要改核心方法,改了核心方法就要怀疑所有已有做法是否被影响。
💭 3 个反思题(先别往下看,自己想 30 秒):
- 如果活动从 3 种涨到 30 种,
calcPrice方法有多长? - 产品说"满减规则要改成满 300 减 40",改哪里?怎么保证没改错?
- 运营想先对 10% 用户灰度"拼团",再全量——if-else 能支持吗?
# 1.3 问题根源拆解
画一张图就清楚了:
flowchart TD
Pay[calcPrice] --> Q{promoType?}
Q --> A1[满减逻辑]
Q --> A2[折扣逻辑]
Q --> A3[包邮逻辑]
Q --> A4[阶梯满减]
Q --> A5[秒杀]
Q --> A6[拼团]
Q --> An[... 30 种 if-else]
style Q fill:#fee
style An fill:#fee
2
3
4
5
6
7
8
9
10
11
所有算法的"选择 + 实现"都集中在一个方法里,互不隔离,这就埋下了 N 类隐患:
| 隐患 | 现象 | 业务影响 |
|---|---|---|
| 开关语句无止境 | 新活动 = 新 else if,每次发版都动核心计价 | 核心服务永远被打扰 |
| 违反开闭原则 | 每改一次促销都要改同一个文件,测试面爆炸 | 改一个活动,30 种活动全部回归 |
| 复用困难 | 下单/购物车预览/订单详情都要算价格,同一套 if-else 复制三处 | 改规则漏同步→前端展示和实际扣款不一致 |
| A/B 测试痛苦 | 想灰度一个新活动 50%→改代码+配置+重发版 | 2 周发版窗口卡死紧急实验 |
| 活动组合更惨 | "满减+新客券"叠加,if-else 嵌套秒炸 | 运营玩法被技术债锁死 |
| 顺序隐式耦合 | 如风控案例:新规则排末尾→前 29 条决定它能否命中 | 670 万资损 |
核心矛盾:业务上"每种活动是一种独立的计价算法",算法族可插拔、可独立测试;但代码层面,所有算法挤在同一个 if-else 栈里——算法选择逻辑 和 算法实现逻辑 没有任何边界。
# 1.4 引出本篇主角
策略模式(Strategy)的核心思想:把每一种"做法"(算法/行为)封装成独立的类。调用方持有策略接口的引用,运行时想用哪种策略就装配哪种——新增策略不再动调用方代码。
interface PricingStrategy {
BigDecimal calc(Order o);
}
// 每种活动是一个独立的策略类
class FullReduceStrategy implements PricingStrategy { public BigDecimal calc(Order o){...} }
class DiscountStrategy implements PricingStrategy { public BigDecimal calc(Order o){...} }
class SeckillStrategy implements PricingStrategy { public BigDecimal calc(Order o){...} }
// 调用方:一行搞定
public BigDecimal calcPrice(Order o) {
PricingStrategy s = strategyRegistry.get(o.getPromoType());
return s.calc(o);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
flowchart LR
Client[calcPrice] --> Ctx[策略注册表]
Ctx --> I[PricingStrategy 接口]
I --> S1[FullReduceStrategy]
I --> S2[DiscountStrategy]
I --> S3[SeckillStrategy]
I --> Sn[新活动随时扩展]
style I fill:#e6f3ff
style Sn fill:#dfd
2
3
4
5
6
7
8
9
和"状态模式"的差别一张图看清(本篇会详谈):
flowchart LR
subgraph 策略["策略: 同一件事的不同算法"]
S1[调用方主动选择] --> S2[策略可自由切换]
end
subgraph 状态["状态: 对象在不同状态下行为不同"]
T1[状态迁移由对象内部驱动] --> T2[状态有生命周期]
end
style S1 fill:#e6f3ff
style T1 fill:#f0e6ff
2
3
4
5
6
7
8
9
Spring 的 BeanNameViewResolver、JDK Comparator、线上的 Tab 实验分流、Netty 的编解码器选择——都是策略模式的经典落地。
但是!先别急着看实现。下一节,我们先看看新人通常会先尝试哪些"看起来很合理"的方案,并理解它们为什么都不够好。
# 02.3次失败探索
为什么要学这一节:直接给你"标准答案"是很容易的,但你要知道,策略模式不是凭空发明的——它是前人走过 3 条死路之后才提炼出来的。走过这些死路,你才会真正理解为什么代码长那个样子。
# 2.1 尝试方案A:继承重写算法
【新人方案①:用一个抽象基类定义 calcPrice,每种促销继承重写】
"既然每种活动算法不一样,我用多态不就行了?父类定义接口,子类各写各的——完美!"
// 基类
abstract class PricingBase {
abstract BigDecimal calc(Order o);
}
// 满减子类
class FullReducePricing extends PricingBase {
BigDecimal calc(Order o) { return o.getAmount().subtract(new BigDecimal("30")); }
}
// 折扣子类
class DiscountPricing extends PricingBase {
BigDecimal calc(Order o) { return o.getAmount().multiply(new BigDecimal("0.8")); }
}
// 秒杀子类
class SeckillPricing extends PricingBase {
BigDecimal calc(Order o) { return o.getAmount().multiply(new BigDecimal("0.5")); }
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
🧪 跑一下,看会出什么问题
// 1. 活动从 3 种涨到 30 种 → 30 个子类,类爆炸
// 2. "满减 + 新客券叠加" → 必须新建一个 FullReduceWithNewUserPricing 子类
// 两种活动的排列组合 = N×M 个子类
// 3. 运行时切换算法 → 必须 new 一个新对象替换旧对象
// 继承关系编译期固定,运行时无法动态切换
// 4. 共享逻辑难复用:满200减30 vs 满500减80
// 都是"满减",但阈值不同 → 必须建两个子类
2
3
4
5
6
7
❌ 失败原因:
- 算法族爆炸:N 种活动 = N 个子类,排列组合 = N×M 个子类;
- 继承是静态的:编译期绑定,运行时无法自由切换策略——想换算法必须 new 新对象;
- 参数化无力:同为"满减",阈值差异就要建两个子类,而非两个参数。
💡 反思:我们要的不是"把算法塞进继承树",而是"运行时自由装配算法对象"——组合优于继承。
# 2.2 尝试方案B:静态 Map + 工厂
【新人方案②:用 Map 存策略实例,用工厂类统一管理】
"我懂了!用组合——搞一个 Map<String, Strategy>,key 是活动类型,value 是策略对象。工厂类统一初始化!"
// 工厂:统一管理所有策略的创建
public class PricingFactory {
private static final Map<String, PricingStrategy> map = new HashMap<>();
static {
map.put("FULL_REDUCE", new FullReduceStrategy());
map.put("DISCOUNT", new DiscountStrategy());
map.put("SECKILL", new SeckillStrategy());
// ... 每加一个新活动,在这里加一行
}
public static PricingStrategy get(String type) {
return map.get(type);
}
}
// 调用方
public BigDecimal calcPrice(Order o) {
PricingStrategy s = PricingFactory.get(o.getPromoType()); // if-else 没了!
return s.calc(o);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
🧪 跑一下,会发现隐藏问题
// 看似很好——但"工厂类的 static 块"成了新的上帝代码:
// 1. 新增活动 → 改 PricingFactory.static{} 加 map.put("XMAS", new XmasStrategy());
// 还是改了同一个文件,只是把 if-else 从 calcPrice 搬到了 factory
// 2. 所有策略对象在 static 块里 new → 启动时就全部加载
// 100 个策略 → 启动慢 + 占用内存
// 3. 张三加了 put("PIN", ...),李四加了 put("TEAM", ...)
// 两人同时改 static 块 → merge 冲突
// 4. Map 的 key 是字符串 "FULL_REDUCE"
// 配置中心写错 → "FULL_REDUCE" vs "full_reduce" → map.get 返回 null → NPE
2
3
4
5
6
7
8
9
❌ 失败原因:
- 上帝类搬家:if-else 从
calcPrice搬到了工厂的static块,还是集中式管理; - 静态初始化:所有策略启动时全部加载,不支持懒加载;
- 字符串 key 无类型安全:大小写/拼写错误编译期不报错;
- 还是需要改工厂类:新增策略仍然需要修改同一个文件。
💡 反思:我们既要去掉 if-else,也要让新增策略时完全不需要修改已有的集中式代码——策略应该自己声明"我是谁",而不是工厂来登记。
# 2.3 尝试方案C:反射 + 类名加载
【新人方案③:策略类名放配置中心,用反射动态加载】
"在配置中心存 promoType → 策略类全限定名 的映射,运行时用 Class.forName 加载——新增活动只改配置,不改代码!"
public class DynamicPricingFactory {
public static PricingStrategy load(String promoType) {
// 从配置中心读取:FULL_REDUCE → com.x.pricing.FullReduceStrategy
String className = configCenter.get("pricing." + promoType);
try {
return (PricingStrategy) Class.forName(className).newInstance(); // ❌
} catch (Exception e) {
throw new RuntimeException("策略加载失败: " + className, e);
}
}
}
2
3
4
5
6
7
8
9
10
11
🧪 跑一下,看会怎样
// 1. 配置中心写错类名: "com.x.pricing.FullRedcueStrategy"
// 编译通过 → 运行时 ClassNotFoundException → 线上炸
// 2. 反射 newInstance() 每次新建对象 → 高频调用 GC 压力大
// 3. 反射绕过了编译期类型检查:
// 类存在但没有实现 PricingStrategy → ClassCastException
// 4. 类名泄漏在配置中心 → 攻击者可以通过配置注入任意类 → 安全漏洞
2
3
4
5
6
❌ 失败原因:
- 类型安全彻底丢失:类名写错/接口不匹配全部运行时暴露;
- 性能损耗:每次反射
newInstance()比直接new慢 ~10×; - 安全风险:类名字符串可被配置注入,执行任意代码;
- 废弃 API:
Class.newInstance()在 Java 9 已废弃(应改用Constructor.newInstance,更复杂)。
💡 反思:必须保留编译期类型安全——策略对象要么由容器管理生命周期,要么由调用方显式选择,绝不能把类型安全交给字符串。
# 2.4 终于引出策略模式
【3 次失败之后,需求清单收敛了】
| 必须满足 | 来自哪一次失败 |
|---|---|
| ① 新增策略时,已有代码零改动(不修改工厂、不修改调用方) | 2.2 方案B |
| ② 运行时自由切换策略(组合,非继承) | 2.1 方案A |
| ③ 编译期类型安全(接口约束,非字符串/反射) | 2.3 方案C |
| ④ 策略无状态 + 可缓存复用 | 2.1 + 2.3 |
【策略模式的标准答案】
// ① 策略接口:编译期类型约束
public interface PricingStrategy { // ③ 接口约束
BigDecimal calc(Order o);
}
// ② 每种算法独立类,互相无感知
@Component("FULL_REDUCE") // ① 自己声明身份
public class FullReduceStrategy implements PricingStrategy {
public BigDecimal calc(Order o) { ... } // ④ 无状态
}
@Component("DISCOUNT")
public class DiscountStrategy implements PricingStrategy {
public BigDecimal calc(Order o) { ... }
}
// ② 调用方:运行时选择策略
public class OrderService {
@Autowired
private Map<String, PricingStrategy> strategyRegistry; // ① Spring 自动注入,零配置
public BigDecimal calcPrice(Order o) {
PricingStrategy s = strategyRegistry.get(o.getPromoType()); // ② 运行时切换
return s.calc(o);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
短短几行,同时回答了上面 4 个需求。这就是策略模式的"灵魂代码"。
# 03.策略模式基础
# 3.1 从失败中提炼需求
回顾 02 节,我们试了继承重写、Map 工厂、反射加载——全部失败。现在拿着这些失败报告,问自己一个问题:
"如果我要写一个能跑 3 年不崩的营销计价系统,它必须满足哪几条硬约束?"
把这些约束写下来,就自然得到了策略模式的设计清单:
| 约束 | 来自 | 代码体现 |
|---|---|---|
| ① 新增策略零改动已有代码 | 2.2 方案B | 策略类用 @Component("name") 自声明,注册表自动发现 |
| ② 运行时自由切换策略 | 2.1 方案A | 组合而非继承,运行时 strategyRegistry.get(type) |
| ③ 编译期类型安全 | 2.3 方案C | PricingStrategy 接口约束,非字符串/反射匹配 |
| ④ 无状态可复用 | 2.1 + 2.3 | 策略对象只含计算逻辑,不含实例字段 |
策略模式(Strategy Pattern):定义一系列算法,将每一个算法封装起来,并让它们可以相互替换。策略模式让算法独立于使用它的客户而变化,也称为政策模式(Policy)。策略模式的重心不是如何实现算法,而是如何组织、调用这些算法——从而让程序结构更灵活。
# 3.2 策略模式的标准骨架
上面 4 条约束翻译成代码,所有实现变体共用一个骨架:
// ===== 策略接口 (Strategy) =====
public interface Strategy<E, R> { // ③ 泛型接口,编译期类型安全
R execute(E context);
}
// ===== 具体策略 (ConcreteStrategy) =====
public class ConcreteStrategyA implements Strategy<Context, Result> {
public Result execute(Context ctx) { // ④ 无状态,纯计算
// 算法A的具体实现
}
}
// ===== 上下文 (Context) =====
public class StrategyContext {
private final Map<String, Strategy> registry; // ② 注册表,运行时选择
public Result doSomething(String type, Context ctx) {
Strategy s = registry.get(type); // ① 新增策略不改这里
return s.execute(ctx);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
三句话记住:① 策略接口定义算法族 → ② 具体策略各自独立实现 → ③ 上下文运行时选一个执行。差异全在 注册表怎么填 / 策略怎么造 / 查不到怎么办 里——这就是下一节 4 种实现的核心分岔。
策略模式包含如下角色:
- Context(环境):持有 Strategy 引用,负责调用策略
- Strategy(抽象策略):接口或抽象类,定义算法族统一签名
- ConcreteStrategy(具体策略):每种算法的独立实现
# 3.3 典型使用场景
不是所有"多分支选一个"都适合策略模式。核心判断标准:"同一件事有多种算法/行为,且算法族会持续增长"。以下场景验证:
- 电商促销计价(本篇主线):满减/折扣/秒杀/拼团/新客券/跨店满减——每种活动是独立的策略类,新增活动只需新加一个
@Component类; - 风控规则引擎(1.1 事故场景):大额拦截/黑名单/设备指纹/地区规则——每条规则是独立策略,独立单测、独立灰度、独立开关;
- 文件排序算法选择(本篇 4.3 详谈):根据文件大小自动选择快排/外部排序/多线程/MapReduce——大小区间 → 算法映射,新增排序算法只加一个类 + 一行配置;
- 支付渠道选择:微信/支付宝/银联/Apple Pay——每种支付渠道是独立策略,新增渠道不改核心支付流程;
- 会员折扣(本篇 4.2 详谈):初级/中级/高级/钻石会员不同折扣——每个等级一个策略类。
反面提醒:分支 ≤ 3 且永不增长、算法之间有强顺序依赖、算法选择本身是核心业务逻辑——这些不是策略模式的典型场景,参考 06 节踩坑实录和 07 节决策树。
# 04.4种实现对比
# 4.1 实现核心要点
4 种写法本质上是在 注册表管理 / 策略生命周期 / 类型安全 / 框架依赖 上的不同取舍。实现策略模式只需三行骨架代码:
// ① 定义策略接口
public interface Strategy { Result execute(Context ctx); }
// ② 查表获取策略
Strategy s = registry.get(type);
// ③ 执行策略
s.execute(ctx);
2
3
4
5
6
7
8
差异全在"registry 是怎么填满的、策略对象是谁 new 的、查不到怎么办"里。下面按演进顺序逐一展开,最后在 7.2 节 会有一张决策图帮你快速定位。
# 4.2 实现A:手写策略接口 + Map 查表
设计权衡:用"手动维护 Map 注册表"换"零框架依赖 + 完全可控"。
选它的理由:新手学习、极简场景、不引入 Spring 的纯 Java 项目。
以电商图书会员折扣为经典案例(初级/中级/高级/钻石会员不同折扣):
// 策略接口
public interface MemberStrategy {
double calcPrice(double booksPrice);
}
// 具体策略
public class PrimaryMemberStrategy implements MemberStrategy {
public double calcPrice(double booksPrice) {
System.out.println("初级会员没有折扣");
return booksPrice;
}
}
public class IntermediateMemberStrategy implements MemberStrategy {
public double calcPrice(double booksPrice) {
System.out.println("中级会员折扣10%");
return booksPrice * 0.9;
}
}
public class AdvancedMemberStrategy implements MemberStrategy {
public double calcPrice(double booksPrice) {
System.out.println("高级会员折扣20%");
return booksPrice * 0.8;
}
}
// Context:持有一个策略引用
public class Price {
private MemberStrategy strategy;
public Price(MemberStrategy strategy) { this.strategy = strategy; }
public double quote(double booksPrice) {
return this.strategy.calcPrice(booksPrice);
}
}
// 测试
MemberStrategy strategy = new AdvancedMemberStrategy();
Price price = new Price(strategy);
double result = price.quote(300); // 输出: 高级会员折扣20%, 240.0
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
技术分析:
- 代码直白,适合教学和 ≤ 5 个策略的场景;
- Context 每次 new 时传入策略——简单但客户端必须知道所有策略类;
- 无注册表:每次需手动
new具体策略 → 策略数量多时不方便; - 无缓存:重复创建策略对象浪费内存——策略无状态时可改为单例 + Map 查表。
# 4.3 实现B:策略 + 工厂 + 缓存 + 查表消除 if-else
设计权衡:用"工厂集中管理 + 查表法"换"彻底消灭 if-else + 策略单例复用"。
选它的理由:纯 Java 项目、策略数量较多(10+)、需要懒加载/缓存。
以文件排序为经典案例(根据文件大小自动选择排序算法):
// 1. 策略接口
public interface ISortAlg {
void sort(String filePath);
}
// 2. 具体策略(各自独立文件,互不依赖)
public class QuickSort implements ISortAlg {
public void sort(String filePath) { /* 快速排序 */ }
}
public class ExternalSort implements ISortAlg {
public void sort(String filePath) { /* 外部排序 */ }
}
public class ConcurrentExternalSort implements ISortAlg {
public void sort(String filePath) { /* 多线程外部排序 */ }
}
public class MapReduceSort implements ISortAlg {
public void sort(String filePath) { /* MapReduce多机排序 */ }
}
// 3. 策略工厂:懒加载 + 缓存 + 查表
public class SortAlgFactory {
private static final Map<String, ISortAlg> cache = new ConcurrentHashMap<>();
public static ISortAlg getSortAlg(String type) {
return cache.computeIfAbsent(type, t -> { // 懒加载 + 缓存
switch (t) {
case "QuickSort": return new QuickSort();
case "ExternalSort": return new ExternalSort();
case "ConcurrentExternalSort": return new ConcurrentExternalSort();
case "MapReduceSort": return new MapReduceSort();
default: throw new IllegalArgumentException("Unknown: " + t);
}
});
}
}
// 4. 查表消除 if-else
public class Sorter {
private static final long GB = 1000 * 1000 * 1000;
private static final List<AlgRange> algs = new ArrayList<>();
static {
algs.add(new AlgRange(0, 6*GB, SortAlgFactory.getSortAlg("QuickSort")));
algs.add(new AlgRange(6*GB, 10*GB, SortAlgFactory.getSortAlg("ExternalSort")));
algs.add(new AlgRange(10*GB, 100*GB, SortAlgFactory.getSortAlg("ConcurrentExternalSort")));
algs.add(new AlgRange(100*GB, Long.MAX_VALUE, SortAlgFactory.getSortAlg("MapReduceSort")));
}
public void sortFile(String filePath) {
long fileSize = new File(filePath).length();
ISortAlg sortAlg = null;
for (AlgRange range : algs) { // 查表,无 if-else!
if (range.inRange(fileSize)) {
sortAlg = range.getAlg();
break;
}
}
sortAlg.sort(filePath); // 策略执行
}
private static class AlgRange {
long start, end;
ISortAlg alg;
boolean inRange(long size) { return size >= start && size < end; }
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
技术分析:
- 查表法:用
List<AlgRange>替代 if-else 链,新增算法只需加一条静态初始化 + 一个策略类; - 懒加载 + 缓存:
computeIfAbsent保证每种策略只创建一次,无状态策略安全复用; - 配置化扩展:大小区间和算法的对应关系可放到配置文件,新增算法只改配置不改代码;
- 工厂仍集中管理:
SortAlgFactory的 switch 是新 if-else——策略再多时考虑 4.4 的 Spring 自动注入。
# 4.4 实现C:Spring 自动注入 Map<String, Strategy>
设计权衡:用"依赖 Spring 容器"换"注册表完全自动化 + 零配置"。
选它的理由:Spring Boot 项目首选,新增策略只需加一个 @Component 类,零配置。
// 1. 策略接口
public interface PricingStrategy {
BigDecimal calc(Order o);
boolean supports(String promoType); // 每个策略声明自己支持哪种类型
}
// 2. 具体策略:用 @Component 自声明,Spring 自动发现
@Component
public class FullReduceStrategy implements PricingStrategy {
@Override
public BigDecimal calc(Order o) {
return o.getAmount().subtract(new BigDecimal("30"));
}
@Override
public boolean supports(String promoType) {
return "FULL_REDUCE".equals(promoType);
}
}
@Component
public class DiscountStrategy implements PricingStrategy {
@Override
public BigDecimal calc(Order o) {
return o.getAmount().multiply(new BigDecimal("0.8"));
}
@Override
public boolean supports(String promoType) {
return "DISCOUNT".equals(promoType);
}
}
// 3. Context: Spring 自动注入所有 Strategy 实现
@Component
public class PricingService {
@Autowired
private List<PricingStrategy> strategies; // Spring 自动注入所有实现类!
public BigDecimal calcPrice(Order o) {
return strategies.stream()
.filter(s -> s.supports(o.getPromoType()))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("未知活动类型: " + o.getPromoType()))
.calc(o);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
或者使用 Map 注入(无需 supports 方法):
@Component
public class PricingService {
@Autowired
private Map<String, PricingStrategy> strategyMap; // key=beanName,value=策略实例
public BigDecimal calcPrice(Order o) {
PricingStrategy s = strategyMap.get(o.getPromoType());
if (s == null) throw new IllegalArgumentException("未知活动类型: " + o.getPromoType());
return s.calc(o);
}
}
2
3
4
5
6
7
8
9
10
11
技术分析:
- 零配置注册表:
@Autowired Map<String, Strategy>Spring 自动扫描所有实现类注入,新增策略只需@Component("PROMO_TYPE")一行; - 生命周期由容器管理:单例、无状态天然安全;
@Order可排序:配合supports()可做优先级匹配(如多个策略都宣称支持同一类型);- 依赖 Spring:非 Spring 项目无法使用。
# 4.5 实现D:枚举绑定策略
设计权衡:用"枚举硬编码策略映射"换"极简 + 类型安全 + switch 编译期完备性检查"。
选它的理由:策略类型固定且数量少(≤ 10 个),永不新增或极少新增。
// 策略接口
interface CalcStrategy {
double calc(double price);
}
// 枚举绑定策略(策略类型固定时最简洁)
public enum MemberLevel {
PRIMARY(price -> price), // 初级: 无折扣
INTERMEDIATE(price -> price * 0.9), // 中级: 9折
ADVANCED(price -> price * 0.8), // 高级: 8折
DIAMOND(price -> price * 0.7); // 钻石: 7折
private final CalcStrategy strategy;
MemberLevel(CalcStrategy strategy) {
this.strategy = strategy;
}
public double calcPrice(double price) {
return strategy.calc(price);
}
}
// 使用: 一行搞定
double price = MemberLevel.ADVANCED.calcPrice(300); // 240.0
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
技术分析:
- 极简:枚举定义策略,无需额外类文件;
- 类型安全:
MemberLevel.xxx编译期校验,不存在字符串拼写错误; - 固定集合:枚举在编译期确定,新增策略需要改枚举类——适合"永不加新"或"极少加新"的场景;
- 适合常量策略:如 HTTP 方法(GET/POST/PUT/DELETE)、线程池拒绝策略等。
# 4.6 4种实现速查表
| 实现方式 | 框架依赖 | 注册表自动化 | 类型安全 | 动态新增 | 推荐度 |
|---|---|---|---|---|---|
| 手写 Map 查表 | ✅ 零依赖 | ❌ 手动维护 | ✅ 编译期 | ✅ 改 Map | ⭐⭐⭐ |
| 策略 + 工厂 + 缓存 + 查表 | ✅ 零依赖 | ❌ 工厂 switch | ✅ 编译期 | ✅ 改工厂 | ⭐⭐⭐⭐ |
| Spring 自动注入 | 需 Spring | ✅ 全自动 | ✅ 编译期 | ✅ 加 @Component 即可 | ⭐⭐⭐⭐⭐ |
| 枚举绑定策略 | ✅ 零依赖 | ✅ 枚举自带 | ✅ 编译期 | ❌ 需改枚举 | ⭐⭐⭐⭐ |
📌 一句话决策:Spring Boot 项目首选 Spring 自动注入,纯 Java 策略多选工厂 + 缓存 + 查表,策略少且固定选枚举绑定,教学选手写 Map。
# 05.用前用后效果对比
为什么单独留一节做对比:很多人记住了"策略模式"几个字,却没算过它到底"省"了多少。下面用 1.1 节的 670 万资损事故做基准,让数据替你回答"为什么要用"。
# 5.1 代码量 & 扩展性对比
实验设定:基线为 1.1 节的风控 if-else(30 条规则硬编码),对比策略模式重构。
// ❌ 用前:RiskEngine.judge() 800+ 行
public RiskResult judge(Transaction tx) {
if (tx.getAmount().compareTo(BIG_AMOUNT) > 0) { ... return BLOCK; }
else if (tx.getCard().isBlackList()) { ... return BLOCK; }
// ... 30 条规则,每加一条 +10~20 行 + 怕影响前 29 条 ...
return PASS;
}
// ✅ 用后:15 行
@Component
public class RiskEngine {
@Autowired
private List<RiskStrategy> strategies; // 自动注入全部规则
public RiskResult judge(Transaction tx) {
for (RiskStrategy s : strategies) {
RiskResult r = s.evaluate(tx);
if (r != RiskResult.PASS) return r; // 命中即返回
}
return RiskResult.PASS;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
📊 12 维实测数据:
| 维度 | ❌ if-else 硬编码(事故现场) | ✅ 策略模式 + 注册表 |
|---|---|---|
judge() 方法行数 | 800+ 行(30 条规则 + 嵌套判断) | 15 行(遍历策略列表 + 短路返回) |
| 新增"东南亚规则"改动 | 在 800 行末尾加 else-if,前 29 条影响新规则 | 新建 SoutheastAsiaRiskStrategy 类,核心 0 改动 |
| 规则顺序控制 | 隐式(写代码的顺序 = 执行顺序) | 显式(@Order(10) / 优先级队列) |
| 单条规则单测 | 几乎不可能(要 mock 800 行环境) | 每条规则独立测试类,5 分钟写完 |
| 单条规则灰度 | 改代码 + 重发版(小王漏判事故根源) | 配置中心一键开关、分流灰度 |
| 规则开关粒度 | 全有或全无(注释代码) | 每条规则独立开关 + 灰度比例 |
| 规则交叉漏洞 | 本次事故(CREDIT vs DEBIT 字符串漏判) | 每条规则独立 explain,正交无歧义 |
| 故障定位时间 | 6 小时(从 14 万笔交易反推哪条规则漏了) | 5 分钟(每笔交易日志带"命中策略 ID + 决策树") |
| A/B 测试能力 | 改代码(小王的事故就是这么来的) | 100% 配置驱动,无需发版 |
| 规则数量上限 | 50 条以上心智负担崩溃 | 1000+ 条无压力(参考阿里风控引擎) |
| 资损风险 | 本次事故 670 万 + 罚金 230 万 | 单条规则 bug 影响范围 = 该规则覆盖流量 |
| 团队协作 | 唯一负责人(小王不敢动其他 29 条) | 规则团队各管各的,PR 互不冲突 |
# 5.2 故障隔离 & 可观测性对比
实验设定:模拟"并发调用 100 次计价,第 5 次跑新加的秒杀策略抛异常":
// ❌ 用前:if-else 里秒杀逻辑写了一个 / by zero → 整个 calcPrice 崩
public BigDecimal calcPrice(Order o) {
if ("FULL_REDUCE".equals(o.getPromoType())) { return ... }
else if ("SECKILL".equals(o.getPromoType())) {
return o.getAmount().divide(BigDecimal.ZERO); // ❌ 崩了,后面全走不到
}
...
}
// ✅ 用后:秒杀策略自己崩 → 其他策略照常
public BigDecimal calcPrice(Order o) {
PricingStrategy s = registry.get(o.getPromoType());
try {
return s.calc(o);
} catch (Exception e) {
log.error("策略[{}]执行失败", o.getPromoType(), e);
return defaultPricing.calc(o); // 兜底策略
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
📊 实测数据:
| 指标 | 用前 | 用后 | 差距 |
|---|---|---|---|
| 单策略异常影响面 | 全部 30 种活动崩 | 只影响该活动 | 30× 缩小 |
| 新增策略上线信心 | 必须跑全量回归 | 只测新策略类 + 冒烟 | 10× 提效 |
| 故障发现时间 | 用户投诉才知 | 日志"策略 ID=xxx 异常"秒级告警 | 分钟级 → 秒级 |
# 5.3 核心收益
🔑 核心收益:策略模式把"同一件事的不同算法"封装在各自独立的类里,调用方只关心"选哪个",不关心"怎么做"——这才是策略模式真正的价值,而不是"消灭了 if-else"。
结论:策略模式的本质是 "把'同一件事的不同算法'拆开,让调用方只负责'选一个',让算法实现者只负责'怎么做',两者通过接口完全解耦"。本次资损 670 万的根因不是"小王代码写错"——而是 30 条规则共享一个执行栈,任何新规则的正确性都依赖前 29 条的执行顺序,这种隐式耦合在团队协作下必然爆炸。改造为策略模式后,每条规则独立类、独立单测、独立配置、独立灰度,新增第 31 条规则的成本从'全员 review 800 行'降到'我自己写一个类'。
# 06.反面踩坑实录
为什么有这一节:01 节让你看到"不用策略模式的痛",但策略模式本身也不是银弹。本节用 6 个真实事故告诉你"乱用的痛"。
# 6.1 踩坑A:策略选择又写成了 if-else
【真实事故】 引入策略模式后把 if-else 从"算法实现"搬到了"策略选择",换汤不换药。
public BigDecimal calc(Order o) {
PricingStrategy s;
if ("FULL_REDUCE".equals(o.getPromoType())) s = new FullReduceStrategy();
else if ("DISCOUNT".equals(o.getPromoType())) s = new DiscountStrategy();
else if ("SECKILL".equals(o.getPromoType())) s = new SeckillStrategy();
// ❌ if-else 又回来了,只是搬了个家
return s.calc(o);
}
2
3
4
5
6
7
8
📌 教训:策略模式的价值不只是"把 if-else 封装成类",更是"让策略选择机制可配置化"。
✅ 正解:用 Map<String, Strategy> 注册表替代 if-else 链;Spring 项目直接用 @Autowired Map<String, Strategy> 自动注入。
# 6.2 踩坑B:策略接口参数失控
【真实事故】 某电商促销引擎 12 种活动复用一个接口,新加"跨店满减"需 4 个新参数→全部 12 个策略类被迫改签名。
interface PricingStrategy {
BigDecimal calc(Order o, User u, Coupon c, Activity a, Channel ch,
DateTime now, RiskScore r, Map<String,Object> ext);
// ❌ 8 个参数,大多数策略只用其中 2-3 个
}
2
3
4
5
📌 教训:不同策略需要的输入差异大时,强行统一接口导致参数膨胀。
✅ 正解:引入 PricingContext 上下文对象封装所有可能输入(Builder 模式按需 set),不需要的字段策略内不读,无副作用。
# 6.3 踩坑C:策略有状态 → 并发超卖
【真实事故】 某秒杀系统库存计数写在策略实例里,Spring singleton 多线程并发 ++count,超卖 327 件 iPhone。
@Service // ❌ 默认 singleton
class SeckillStrategy implements PricingStrategy {
private int count = 0; // ❌ 实例字段(有状态)
public BigDecimal calc(Order o) {
if (++count > LIMIT) return o.getAmount();
return o.getAmount().multiply(BigDecimal.valueOf(0.5));
}
}
2
3
4
5
6
7
8
📌 教训:策略类在注册表/Spring 中通常被复用为单例,任何实例字段都是共享状态。
✅ 正解:策略类必须无状态;状态走外部存储(Redis/DB/ThreadLocal);确需有状态用 @Scope("prototype");单测必加并发测试。
# 6.4 踩坑D:策略数量爆炸
【真实事故】 把"策略"和"参数"混淆——满 200 减 30、满 500 减 80、满 1000 减 200 各建一个类,60+ 个文件。
src/main/java/com/x/strategy/pricing/
├── FullReduce200_30Strategy.java
├── FullReduce500_80Strategy.java
├── FullReduce1000_200Strategy.java
└── ... 60+ 个文件
2
3
4
5
📌 教训:参数差异不应成为独立策略——只有算法差异才独立成类。
✅ 正解:参数化策略——new FullReduceStrategy(threshold=200, off=30);配置驱动——满减规则放数据库,运行时实例化通用策略;类爆炸时考虑模板方法 + 策略组合。
# 6.5 踩坑E:没有兜底 → 空指针崩站
【真实事故】 运营在配置中心新加 promoType="XMAS" 但开发没同步上线策略类,圣诞节当天首页所有商品报 NPE 整站崩溃。
PricingStrategy s = strategies.get(o.getPromoType());
return s.calc(o); // ❌ promoType 不在表里 → s = null → NPE
2
📌 教训:注册表查不到时必须兜底,配置和策略类上线必须同步。
✅ 正解:查不到时返回 DefaultStrategy(空对象模式);启动时自检所有 promoType 必须有对应策略,否则启动失败;配置变更走灰度 + 审批流。
# 6.6 踩坑F:只有 2 个分支也上策略
【真实事故】 只有 USER/VIP 两种类型且 5 年没加过新类型,却搞了 4 个文件(接口 + 2 个实现 + 工厂 + 注册表)。
// 一个 if-else 能解决的问题被搞成了 4 个文件 + 4 个类
interface UserPriceStrategy { BigDecimal calc(Order o); }
class NormalUserStrategy implements UserPriceStrategy { ... }
class VipUserStrategy implements UserPriceStrategy { ... }
class StrategyFactory { ... }
// ❌ 过度设计
2
3
4
5
6
📌 教训:分支数 ≤ 3 且不会增长,直接 if-else 更清晰——模式是为了解决问题,不是炫技。
✅ 正解:分支 ≥ 5 或预计还会扩展,再上策略;否则三元表达式或简单 if-else 即可。
# 6.7 开源案例速查 & 替代方案汇总
🔍 14 个真实开源/框架中的策略模式
| 出处 | 策略接口 | 具体策略 | 它解决了什么 |
|---|---|---|---|
JDK Comparator<T> | compare(a,b) | Comparator.comparing / 自定义 | 排序算法的差异化(自然序/字典序/自定义) |
JDK Cipher | getInstance(algo) | AES / RSA / DES | 加密算法族切换 |
JDK Charset | Charset.forName | UTF-8 / GBK / ISO-8859-1 | 字符编码族切换 |
JDK RejectedExecutionHandler | rejectedExecution | AbortPolicy / CallerRunsPolicy / DiscardPolicy / DiscardOldestPolicy | 线程池拒绝策略 |
Spring HandlerMapping | getHandler(req) | RequestMapping / SimpleUrl / BeanNameUrl | URL 到 Controller 的多种映射策略 |
Spring HttpMessageConverter | read/write | MappingJackson2 / FormHttp / ResourceHttp | 请求/响应体的多种序列化策略 |
Spring ConversionService | convert(s,t) | 各种 Converter<S,T> | 类型转换策略族 |
Spring Boot HealthIndicator | health() | DataSource / Disk / Redis / Mongo | 健康检查的多种探针策略 |
Spring Cloud LoadBalancer | choose() | Round-Robin / Random / Weighted | 客户端负载均衡策略 |
Netty EventLoopGroup | register/select | Nio / Epoll / KQueue | I/O 多路复用策略(按操作系统切换) |
MyBatis Executor | update/query | Simple / Reuse / Batch | SQL 执行策略 |
Tomcat Authenticator | authenticate | Basic / Digest / Form / SSL | HTTP 认证策略族 |
Hadoop CompressionCodec | createStream | Gzip / Snappy / LZO / Bzip2 | 压缩算法族 |
Java NIO FileSystemProvider | newFileSystem | 默认 / Zip / 自定义 | 文件系统抽象策略 |
⚠️ 替代方案:什么时候不该用策略
| 你的需求 | 推荐方案 |
|---|---|
| 分支 ≤ 3 且永不增长 | ✅ 简单 if-else 或三元表达式 |
| 算法之间需共享大量上下文 | ✅ 引入 Context 对象,否则别强上策略 |
| 算法选择本身就是核心业务逻辑 | ✅ 策略模式只是换了表达方式,if-else 未必更差 |
| 追求极致性能(热路径) | ✅ 直接 if-else,策略查表 + 虚函数调用慢 ~10ns |
| 算法间有强先后依赖 | ✅ 责任链或工作流,而非"选其一" |
| 状态驱动而非客户端选择 | ✅ 状态模式,不是策略 |
学习路径:先读 JDK
Comparator(最简洁的策略,一行 lambda)→ 再读RejectedExecutionHandler(4 个具名策略 + 自定义扩展,是策略模式 + 空对象的混合范式)→ 进阶读 SpringHttpMessageConverter(带优先级 + Content-Type 匹配 + 注册表)→ 最后读 NettyEventLoopGroup(按操作系统选择不同 I/O 多路复用,是策略 + 工厂 + SPI 的工业级组合)。
# 07.决策树与选型
经过前面 6 节的铺垫,是时候给一张能"贴在工位上"的决策图了。
# 7.1 该不该用策略模式
flowchart TD
Start(["同一件事有多种做法<br/>(算法/行为族)"]) --> Q1{"分支数是否 ≥ 5<br/>或预计会持续增长?"}
Q1 -->|否| Q1b{"分支数 ≤ 3 且<br/>5 年内不会加?"}
Q1b -->|是| No1["❌ 简单 if-else 更清晰<br/>引入策略是过度设计"]
Q1b -->|否| Q2{"算法之间<br/>有强先后依赖吗?"}
Q1 -->|是| Q2
Q2 -->|是| Warn["⚠️ 考虑责任链或工作流<br/>策略选'一个'而非'依次执行'"]
Q2 -->|否| Q3{"算法选择是由<br/>对象内部状态驱动的吗?"}
Q3 -->|是| Alt["⚠️ 那是状态模式的地盘<br/>不是策略"]
Q3 -->|否| Q4{"需要热路径<br/>极致性能吗?"}
Q4 -->|是| Alt2["⚠️ 直接 if-else + 基准测试<br/>查表+虚函数调用慢~10ns"]
Q4 -->|否| Solution["✅ 用策略模式!<br/>算法族独立封装 + 可插拔"]
style No1 fill:#fee
style Warn fill:#ffe6cc
style Alt fill:#ffe6cc
style Alt2 fill:#ffe6cc
style Solution fill:#dfd
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 7.2 选哪种实现方式
如果决策树走到了"用策略模式",再用下面这张图选具体实现:
flowchart TD
Start([选择策略的实现方式]) --> Q1{"是否使用 Spring?"}
Q1 -->|是| Q1b{"策略数量<br/>会持续增长吗?"}
Q1b -->|是| SpringMap["✅ @Autowired Map<String,Strategy><br/>零配置自动注入<br/>新增只需 @Component"]
Q1b -->|否| Q1c{"策略类型<br/>是否固定(≤10个)?"}
Q1c -->|是| EnumS["枚举绑定策略<br/>编译期类型安全<br/>极简无额外文件"]
Q1c -->|否| SpringMap
Q1 -->|否| Q2{"策略数量 ≥ 10<br/>且会增长?"}
Q2 -->|是| Factory["✅ 工厂 + 缓存 + 查表<br/>懒加载复用 + 配置化映射<br/>消除 if-else 选择"]
Q2 -->|否| Q3{"策略类型固定<br/>且 ≤ 10 个?"}
Q3 -->|是| EnumS2["枚举绑定策略<br/>或手写 Map 查表"]
Q3 -->|否| HandMap["手写 Map 查表<br/>轻量 + 零依赖"]
style SpringMap fill:#e6ffe6
style Factory fill:#fff4e6
style EnumS fill:#f0e6ff
style EnumS2 fill:#f0e6ff
style HandMap fill:#e6f3ff
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 7.3 选型清单速查
| 场景 | 该用吗 | 推荐方式 |
|---|---|---|
| Spring Boot 电商计价(满减/折扣/秒杀 30+ 种活动) | ✅ 该用 | @Autowired Map<String,Strategy> 自动注入 |
| 风控规则引擎(30+ 条规则,持续新增) | ✅ 该用 | Spring 自动注入 + @Order 优先级 |
| 纯 Java 工具库,文件排序算法选择 | ✅ 该用 | 工厂 + 缓存 + 查表(AlgRange 列表) |
| 支付渠道选择(微信/支付宝/银联 ≤ 5 种) | ✅ 该用 | 枚举绑定 或 Spring 自动注入 |
| 只有 VIP/普通两种类型,5 年没变过 | ❌ 别用 | 简单 if-else |
| 审批流:A→B→C 接力处理 | ❌ 别用 | 责任链模式,不是"选一个" |
| 订单状态机(待支付→已支付→已发货) | ❌ 别用 | 状态模式,不是策略 |
| 会员折扣(初级/中级/高级/钻石 固定 4 种) | ⚠️ 可选 | 枚举绑定最简,或手写 Map |
# 08.总结与延伸
# 8.1 设计思想沉淀
回顾本篇 1 → 7 节的旅程,策略模式真正教会我们的是这套思考模型:
| 阶段 | 学到了什么 |
|---|---|
| 01 案例引入 | 痛点是模式诞生的土壤——670 万资损的本质是"算法族+K种算法的选择"挤在同一个 if-else 栈里 |
| 02 3次失败 | 继承重写 / Map工厂 / 反射加载都不够——模式是从"试错"中收敛出来的 |
| 03 模式基础 | 四大硬约束:零改动新增 / 运行时切换 / 类型安全 / 无状态 |
| 04 4种实现 | 实现差异本质是"注册表管理 / 生命周期 / 框架依赖"的不同权衡 |
| 05 效果对比 | 数据说话:单策略异常影响面缩小 30×,故障定位从 6 小时降到 5 分钟 |
| 06 反面踩坑 | 策略不是免死金牌——if-else搬家 / 参数膨胀 / 有状态 / 类爆炸 / 无兜底 / 过度设计 |
| 07 决策树 | 工程师的成熟度,不在于会写几种实现,而在于知道"什么时候不写" |
🔑 一句话核心:
策略模式是用来管理"同一件事的不同算法族"的解耦机制,不是任何 if-else 的替代品——分支极少 / 算法间强耦合 / 极致性能场景下,朴素 if-else 反而是最佳解。
# 8.2 模式联动边界
策略模式从来不是孤立存在的,它和其他模式有千丝万缕的关系:
flowchart LR
Strategy[策略] -.客户端选一个.-> State[状态]
Strategy -.组合替代继承.-> Template[模板方法]
Strategy -.常配合创建.-> Factory[工厂]
Strategy -.策略匹配可用.-> Chain[责任链]
State -.内部状态驱动.-> Chain
2
3
4
5
6
| 模式 | 关系 | 一句话区别 |
|---|---|---|
| 策略(Strategy) | 同一件事的不同算法 | 客户端主动选一个策略,随时可换,无状态 |
| 状态(State) | 结构相似,意图不同 | 对象内部驱动,状态机迁移触发,有状态 |
| 模板方法(Template) | 组合 vs 继承 | 模板用继承固定骨架留钩子;策略用组合替换整段算法 |
| 工厂方法(Factory) | 常与策略配合 | 工厂只负责"造对象";策略负责"执行算法"——两者常组合 |
| 责任链(Chain) | 策略匹配可用责任链 | 责任链是接力"谁处理谁处理";策略是"选一个" |
| 命令(Command) | 不同维度 | 命令把请求封装成对象(可撤销/排队);策略是算法族封装(可替换) |
一句话区分:
- 同一件事的多种"做法",客户端选一个 → 策略;
- 同一对象在不同状态下行为不同,状态自动迁移 → 状态;
- 流程骨架固定,变化点是步骤 → 模板方法;
- 只是"造对象" → 工厂;
- 多个处理者接力,只一个响应 → 责任链;
- 请求需要排队/撤销 → 命令。
# 8.3 思考题与延伸
💭 三道思考题(建议手写答案,再对照回顾本文):
- 为什么 Spring 的
HandlerMapping用策略模式而不是状态模式?(提示:回看 6.7 开源案例表和 8.2 联动边界——URL 到 Controller 的映射是"客户端选中一个"还是"内部状态切换"?) - 风控引擎 30 条规则用策略模式后,如果要求"大额规则必须先于黑名单规则执行",你该加什么机制?(提示:回看 4.4 的
@Order注解) new FullReduceStrategy(200, 30)和new FullReduce200_30Strategy(),哪个是策略模式的正用,哪个是滥用?为什么?(提示:回看 6.4 踩坑D)
📚 延伸阅读:
- JDK
Comparator源码——最简洁的策略模式,一行 lambda 即可 - JDK
RejectedExecutionHandler源码——4 个具名策略 + 空对象模式的混合范式 - Spring
HttpMessageConverter源码——带优先级 + Content-Type 匹配 + 注册表的工业级策略 - 《Design Patterns》GoF 原著 Chapter 5:Strategy
上一篇 观察者模式设计思想 → 本篇 → 下一篇:模板方法模式设计思想——用"算法骨架 + 子类填空"解决流程复用与差异化并存的经典场景。