编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • 面向对象设计

  • 常见设计原则

  • 巧学设计模式

    • README
    • 单例模式设计思想
    • 工厂模式设计思想
    • 建造者模式设计思想
    • 原型模式设计思想
    • 静态代理设计模式
    • 动态代理设计模式
    • 适配器模式设计思想
    • 装饰者模式设计思想
    • 外观模式设计思想
    • 桥接模式设计思想
    • 组合模式设计思想
    • 享元模式设计思想
    • 观察者模式设计思想
    • 策略者模式设计思想
      • 推荐一个好玩网站
      • 01.案例引入与思考
        • 1.1 痛点现场
        • 1.2 直觉实现复现
        • 1.3 问题根源拆解
        • 1.4 引出本篇主角
      • 02.3次失败探索
        • 2.1 尝试方案A:继承重写算法
        • 2.2 尝试方案B:静态 Map + 工厂
        • 2.3 尝试方案C:反射 + 类名加载
        • 2.4 终于引出策略模式
      • 03.策略模式基础
        • 3.1 从失败中提炼需求
        • 3.2 策略模式的标准骨架
        • 3.3 典型使用场景
      • 04.4种实现对比
        • 4.1 实现核心要点
        • 4.2 实现A:手写策略接口 + Map 查表
        • 4.3 实现B:策略 + 工厂 + 缓存 + 查表消除 if-else
        • 4.4 实现C:Spring 自动注入 Map<String, Strategy\>
        • 4.5 实现D:枚举绑定策略
        • 4.6 4种实现速查表
      • 05.用前用后效果对比
        • 5.1 代码量 & 扩展性对比
        • 5.2 故障隔离 & 可观测性对比
        • 5.3 核心收益
      • 06.反面踩坑实录
        • 6.1 踩坑A:策略选择又写成了 if-else
        • 6.2 踩坑B:策略接口参数失控
        • 6.3 踩坑C:策略有状态 → 并发超卖
        • 6.4 踩坑D:策略数量爆炸
        • 6.5 踩坑E:没有兜底 → 空指针崩站
        • 6.6 踩坑F:只有 2 个分支也上策略
        • 6.7 开源案例速查 & 替代方案汇总
      • 07.决策树与选型
        • 7.1 该不该用策略模式
        • 7.2 选哪种实现方式
        • 7.3 选型清单速查
      • 08.总结与延伸
        • 8.1 设计思想沉淀
        • 8.2 模式联动边界
        • 8.3 思考题与延伸
    • 模版模式设计思想
    • 迭代器模式设计思想
    • 职责链模式设计思想
    • 命令模式设计思想
    • 状态模式设计思想
    • 备忘录模式设计思想
    • 中介者模式设计思想
    • 访问者模式设计思想
    • 解释器模式设计思想
    • 23种设计模式概括
    • 技术写作模板
  • 系统架构设计

  • 编程
  • 巧学设计模式
杨充
2023-10-07
目录

策略者模式设计思想

# 14.策略者模式设计思想

📚 本篇渐进学习节奏(建议按顺序食用)

  1. 第 01 节 · 案例引入 — 周日 03:14 风控系统 if-else 30 个分支,新增"东南亚规则"被压在末尾,黑产 6 小时刷走 670 万的 P0 资损事故
  2. 第 02 节 · 3 次失败探索 — 继承重写 / Map 工厂 / 反射加载,三种直觉方案为何全部翻车
  3. 第 03 节 · 模式基础 — 从失败清单逆推设计约束 → 标准骨架 Context/Strategy/ConcreteStrategy → 场景识别
  4. 第 04 节 · 4 种实现对比 — 手写 Map 查表 → 工厂 + 缓存 → Spring 自动注入 → 枚举绑定
  5. 第 05 节 · 效果对比 — 用前用后 12 维数据说话(事故现场 vs 策略模式重构)
  6. 第 06 节 · 反面踩坑 — 6 种翻车姿势实录 + 14 个开源案例 + 替代方案汇总
  7. 第 07 节 · 决策树 — 该不该用 → 用哪种实现 → 速查清单,贴工位上就能用
  8. 第 08 节 · 总结延伸 — 思考模型沉淀 + 与状态/模板方法/工厂/责任链的精准切割 + 3 道自测题

阅读到任一节卡壳,直接跳回上一节复盘场景;本篇所有代码均可直接运行。

# 目录介绍

  • 01.案例引入与思考
    • 1.1 痛点现场
    • 1.2 直觉实现复现
    • 1.3 问题根源拆解
    • 1.4 引出本篇主角
  • 02.3次失败探索
    • 2.1 尝试方案A:继承重写算法
    • 2.2 尝试方案B:静态 Map + 工厂
    • 2.3 尝试方案C:反射 + 类名加载
    • 2.4 终于引出策略模式
  • 03.策略模式基础
    • 3.1 从失败中提炼需求
    • 3.2 策略模式的标准骨架
    • 3.3 典型使用场景
  • 04.4种实现对比
    • 4.1 实现核心要点
    • 4.2 实现A:手写策略接口 + Map 查表
    • 4.3 实现B:策略 + 工厂 + 缓存 + 查表消除 if-else
    • 4.4 实现C:Spring 自动注入 Map<String, Strategy>
    • 4.5 实现D:枚举绑定策略
    • 4.6 4种实现速查表
  • 05.用前用后效果对比
    • 5.1 代码量 & 扩展性对比
    • 5.2 故障隔离 & 可观测性对比
    • 5.3 核心收益
  • 06.反面踩坑实录
    • 6.1 踩坑A:策略选择又写成了 if-else
    • 6.2 踩坑B:策略接口参数失控
    • 6.3 踩坑C:策略有状态 → 并发超卖
    • 6.4 踩坑D:策略数量爆炸
    • 6.5 踩坑E:没有兜底 → 空指针崩站
    • 6.6 踩坑F:只有 2 个分支也上策略
    • 6.7 开源案例速查 & 替代方案汇总
  • 07.决策树与选型
    • 7.1 该不该用策略模式
    • 7.2 选哪种实现方式
    • 7.3 选型清单速查
  • 08.总结与延伸
    • 8.1 设计思想沉淀
    • 8.2 模式联动边界
    • 8.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 个,一个比一个炸裂:

  1. 顺序错位:第 30 条规则被压在最末尾,前面"3DS 豁免规则"对所有国家都适用,且 amount > 500 才拦截,黑产精准选择 50-200 元绕过;
  2. 规则交叉漏判:黑产研究了公开 SDK 文档,发现东南亚 + 借记卡是漏洞,改用借记卡(不匹配 CREDIT)发起 49.9 元交易,完美绕过新规则;
  3. 改一行牵全身:小王上线前对前 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();
}
1
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
1
2
3
4
5
6
7
8

事故现场重现完毕——"多种算法的选择逻辑"和"每种算法的具体实现"被焊死在同一个方法里,新加一种做法就要改核心方法,改了核心方法就要怀疑所有已有做法是否被影响。

💭 3 个反思题(先别往下看,自己想 30 秒):

  1. 如果活动从 3 种涨到 30 种,calcPrice 方法有多长?
  2. 产品说"满减规则要改成满 300 减 40",改哪里?怎么保证没改错?
  3. 运营想先对 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
1
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);
}
1
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
1
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
1
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")); }
}
1
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
//    都是"满减",但阈值不同 → 必须建两个子类
1
2
3
4
5
6
7

❌ 失败原因:

  1. 算法族爆炸:N 种活动 = N 个子类,排列组合 = N×M 个子类;
  2. 继承是静态的:编译期绑定,运行时无法自由切换策略——想换算法必须 new 新对象;
  3. 参数化无力:同为"满减",阈值差异就要建两个子类,而非两个参数。

💡 反思:我们要的不是"把算法塞进继承树",而是"运行时自由装配算法对象"——组合优于继承。

# 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);
}
1
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
1
2
3
4
5
6
7
8
9

❌ 失败原因:

  1. 上帝类搬家:if-else 从 calcPrice 搬到了工厂的 static 块,还是集中式管理;
  2. 静态初始化:所有策略启动时全部加载,不支持懒加载;
  3. 字符串 key 无类型安全:大小写/拼写错误编译期不报错;
  4. 还是需要改工厂类:新增策略仍然需要修改同一个文件。

💡 反思:我们既要去掉 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);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11

🧪 跑一下,看会怎样

// 1. 配置中心写错类名: "com.x.pricing.FullRedcueStrategy"
//    编译通过 → 运行时 ClassNotFoundException → 线上炸
// 2. 反射 newInstance() 每次新建对象 → 高频调用 GC 压力大
// 3. 反射绕过了编译期类型检查:
//    类存在但没有实现 PricingStrategy → ClassCastException
// 4. 类名泄漏在配置中心 → 攻击者可以通过配置注入任意类 → 安全漏洞
1
2
3
4
5
6

❌ 失败原因:

  1. 类型安全彻底丢失:类名写错/接口不匹配全部运行时暴露;
  2. 性能损耗:每次反射 newInstance() 比直接 new 慢 ~10×;
  3. 安全风险:类名字符串可被配置注入,执行任意代码;
  4. 废弃 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);
    }
}
1
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);
    }
}
1
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);
1
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
1
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; }
    }
}
1
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);
    }
}
1
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);
    }
}
1
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
1
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;
    }
}
1
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);   // 兜底策略
    }
}
1
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);
}
1
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 个
}
1
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));
    }
}
1
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+ 个文件
1
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
1
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 { ... }
// ❌ 过度设计
1
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 个具名策略 + 自定义扩展,是策略模式 + 空对象的混合范式)→ 进阶读 Spring HttpMessageConverter(带优先级 + Content-Type 匹配 + 注册表)→ 最后读 Netty EventLoopGroup(按操作系统选择不同 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
1
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&lt;String,Strategy&gt;<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
1
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
1
2
3
4
5
6
模式 关系 一句话区别
策略(Strategy) 同一件事的不同算法 客户端主动选一个策略,随时可换,无状态
状态(State) 结构相似,意图不同 对象内部驱动,状态机迁移触发,有状态
模板方法(Template) 组合 vs 继承 模板用继承固定骨架留钩子;策略用组合替换整段算法
工厂方法(Factory) 常与策略配合 工厂只负责"造对象";策略负责"执行算法"——两者常组合
责任链(Chain) 策略匹配可用责任链 责任链是接力"谁处理谁处理";策略是"选一个"
命令(Command) 不同维度 命令把请求封装成对象(可撤销/排队);策略是算法族封装(可替换)

一句话区分:

  • 同一件事的多种"做法",客户端选一个 → 策略;
  • 同一对象在不同状态下行为不同,状态自动迁移 → 状态;
  • 流程骨架固定,变化点是步骤 → 模板方法;
  • 只是"造对象" → 工厂;
  • 多个处理者接力,只一个响应 → 责任链;
  • 请求需要排队/撤销 → 命令。

# 8.3 思考题与延伸

💭 三道思考题(建议手写答案,再对照回顾本文):

  1. 为什么 Spring 的 HandlerMapping 用策略模式而不是状态模式?(提示:回看 6.7 开源案例表和 8.2 联动边界——URL 到 Controller 的映射是"客户端选中一个"还是"内部状态切换"?)
  2. 风控引擎 30 条规则用策略模式后,如果要求"大额规则必须先于黑名单规则执行",你该加什么机制?(提示:回看 4.4 的 @Order 注解)
  3. 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

上一篇 观察者模式设计思想 → 本篇 → 下一篇:模板方法模式设计思想——用"算法骨架 + 子类填空"解决流程复用与差异化并存的经典场景。

上次更新: 2026/06/17, 11:43:57
观察者模式设计思想
模版模式设计思想

← 观察者模式设计思想 模版模式设计思想→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式