小明学架构师的第七课:策略模式:告别if-else的选择困难症
行为型模式的开篇之作。 如果要用一句话概括策略模式:把"怎么做"的选择,从if-else里解放出来。一个接口,多个实现,运行时随意切换——这就是策略。
一、一句话理解策略
| 模式 | 一句话 | 生活类比 |
|---|---|---|
| 策略 Strategy | 定义一组算法,封装每个算法,让它们可以互相替换 | 导航App:最短路线/避开高速/少收费——目标一样,策略不同 |
💡策略模式的核心思想:
把一个业务目标拆成多种实现方式,每种方式封装成一个独立的"策略对象"。 上下文(Context)持有策略接口,运行时根据需要切换具体策略。三个角色:
- Strategy 接口:定义所有策略的统一方法
- ConcreteStrategy 具体策略:每种算法一个类
- Context 上下文:持有策略引用,委托执行
一句话:算法变了,我只换策略对象,不动主流程。
二、小明的if-else地狱
2.1 产品经理的一句话
📖故事
九月初,老板开完会,拍着小明的肩膀说: "双十一搞个大的,优惠玩法要丰富!" 产品经理第二天甩来一份需求文档: ① 满200减30 ② 全场8折 ③ 秒杀价(固定价格) ④ 新人首单5折 ⑤ VIP会员9折 ⑥ 拼团立减15 ⑦ 积分抵扣(100积分=1元)小明看着这份清单,心想:"不就是7个if-else嘛,半天搞定。"
2.2 代码写成了一堵墙
两天后,代码写完了。
public double calculateDiscount(Order order) { String type = order.getPromoType(); double price = order.getOriginalPrice(); if ("FULL_REDUCTION".equals(type)) { // 满200减30 if (price >= 200) { return price - 30; } return price; } else if ("PERCENT_OFF".equals(type)) { // 全场8折 return price * 0.8; } else if ("FLASH_SALE".equals(type)) { // 秒杀价 return order.getFlashSalePrice(); } else if ("NEW_USER".equals(type)) { // 新人首单5折 if (userService.isNewUser(order.getUserId())) { return price * 0.5; } return price; } else if ("VIP".equals(type)) { // VIP会员9折 if (userService.isVip(order.getUserId())) { return price * 0.9; } return price; } else if ("GROUP_BUY".equals(type)) { // 拼团立减15 if (groupBuyService.isInGroup(order.getGroupId())) { return price - 15; } return price; } else if ("POINTS".equals(type)) { // 积分抵扣 int points = userService.getPoints(order.getUserId()); double deduction = points / 100.0; return Math.max(price - deduction, 0); } else { return price; } }方法体80行。加上各种辅助判断逻辑,整个类超过500行。
💥问题来了:
① 测试同事崩溃:每加一种策略,所有分支都要重新回归测试 ② 产品经理又来了:"再加一个直播专享价,再加一个品类专属折扣…" ③ 运营说:"满减规则要改成阶梯式的,满100减10,满200减30,满500减80" ④ 小明每次改代码,都怕不小心改坏了别的分支
这就是典型的违反开闭原则:每次新增策略,都要修改这个核心方法。改得越多,Bug越多。
三、策略模式重构
3.1 第一步:抽出接口
小明想清楚了:每种优惠算法,本质上都是"输入原价,输出折后价"。
// 策略接口:统一的方法签名 public interface PromotionStrategy { /** * 计算优惠后价格 * @param order 订单信息 * @return 优惠后价格 */ double calculate(Order order); /** * 策略类型标识(用于工厂匹配) */ String getType(); }3.2 第二步:每个策略一个类
// 策略1:满减 public class FullReductionStrategy implements PromotionStrategy { @Override public double calculate(Order order) { double price = order.getOriginalPrice(); if (price >= 200) { return price - 30; } return price; } @Override public String getType() { return "FULL_REDUCTION"; } } // 策略2:折扣 public class PercentOffStrategy implements PromotionStrategy { @Override public double calculate(Order order) { return order.getOriginalPrice() * 0.8; } @Override public String getType() { return "PERCENT_OFF"; } } // 策略3:秒杀 public class FlashSaleStrategy implements PromotionStrategy { @Override public double calculate(Order order) { return order.getFlashSalePrice(); } @Override public String getType() { return "FLASH_SALE"; } } // 策略4:新人首单 public class NewUserStrategy implements PromotionStrategy { @Autowired private UserService userService; @Override public double calculate(Order order) { if (userService.isNewUser(order.getUserId())) { return order.getOriginalPrice() * 0.5; } return order.getOriginalPrice(); } @Override public String getType() { return "NEW_USER"; } } // ... 其他策略类类似,每个类只做一件事✅关键变化:
每个策略类只负责一种优惠算法。 改满减规则?只改 FullReductionStrategy 一个类,其他纹丝不动。 新人首单逻辑有Bug?只影响 NewUserStrategy,不会波及VIP和拼团。
3.3 第三步:用Map注册表消灭if-else
策略类写好了,但选择逻辑怎么办?还是得 if-else 判断 type 然后 new 对应的策略?
不。用Map注册表。
@Component public class PromotionContext { // 策略注册表:type → 策略实例 private final Map<String, PromotionStrategy> strategyMap = new HashMap<>(); // Spring 自动注入所有 PromotionStrategy 实现类 @Autowired public PromotionContext(List<PromotionStrategy> strategies) { for (PromotionStrategy strategy : strategies) { strategyMap.put(strategy.getType(), strategy); } } /** * 执行优惠计算 * 一行代码搞定,没有 if-else */ public double applyPromotion(Order order) { PromotionStrategy strategy = strategyMap .get(order.getPromoType()); if (strategy == null) { log.warn("未知优惠类型: {}", order.getPromoType()); return order.getOriginalPrice(); // 默认原价 } return strategy.calculate(order); } }🔥这段代码的精髓:
① Spring 启动时,自动扫描所有 PromotionStrategy 的实现类,注入到 List 里 ② 构造函数里遍历 List,以 getType() 为 key 注册到 Map ③ 调用时,一行 get(type) 就拿到对应策略,** 零 if-else**
3.4 效果:加新策略只要5分钟
📖故事
十月中旬,产品经理又来找小明: "双十一加了直播渠道,要支持'直播专享价'。" 以前:改500行的核心方法,加一个 else if 分支,然后全量回归测试。 现在:**第一步 **,新建一个类:
public class LiveStreamStrategy implements PromotionStrategy { @Override public double calculate(Order order) { // 直播专享价:原价减20,最低1元 double price = order.getOriginalPrice(); return Math.max(price - 20, 1.0); } @Override public String getType() { return "LIVE_STREAM"; } }**第二步 **,加个 @Component 注解。
完事了。
Spring 启动时自动扫描、自动注册。PromotionContext 里的 strategyMap 自动多了一条 "LIVE_STREAM" 的记录。
小明泡了杯茶,对产品经理说:"上了。"产品经理:"???不需要改别的代码?"小明:"不需要。"
四、对比一下
| 维度 | if-else 版本 | 策略模式版本 |
|---|---|---|
| 新增策略 | 修改核心方法,加 else if | 新建一个类,加 @Component |
| 修改策略 | 在500行代码里定位,容易改错 | 改一个类,影响范围明确 |
| 单元测试 | 要覆盖所有分支 | 每个策略类独立测试 |
| 代码行数 | 1个方法80行,整个类500+行 | 每个策略类20-30行 |
| 开闭原则 | ❌ 违反(修改已有代码) | ✅ 符合(扩展新类,不改旧代码) |
| 运行时切换 | 需要重新调方法 | 换策略对象就行 |
五、策略模式的代价
策略模式不是银弹。它也有代价:
5.1 类数量膨胀
7种优惠 = 7个策略类。如果优惠类型有20种,就是20个类。
但换个角度想:** 20个小类 vs 1个2000行的大类,你选哪个?**
5.2 客户端必须知道所有策略
调用方需要知道有哪些策略可选。但这个问题已经被 Map 注册表解决了——前端传 type,后端自动匹配。
5.3 策略之间不能共享逻辑
如果多个策略有相同的判断逻辑(比如都要检查用户身份),可以抽一个抽象基类:
// 抽象策略基类:共享通用逻辑 public abstract class AbstractPromotionStrategy implements PromotionStrategy { @Autowired protected UserService userService; // 通用方法:检查是否可用 protected boolean isAvailable(Order order) { return order != null && order.getOriginalPrice() > 0; } } // 具体策略继承抽象基类 public class VipStrategy extends AbstractPromotionStrategy { @Override public double calculate(Order order) { if (!isAvailable(order)) return 0; if (!userService.isVip(order.getUserId())) { return order.getOriginalPrice(); } return order.getOriginalPrice() * 0.9; } @Override public String getType() { return "VIP"; } }💡策略 + 模板方法 = 组合拳
抽象基类提供通用骨架(模板方法),子类覆写差异部分。 这个组合在框架设计中非常常见。后面讲模板方法模式时会展开说。
六、Java 8 函数式简化
如果策略只有一个方法(绝大多数情况),可以用 Lambda 表达式直接简化:
// 不需要定义接口和类,直接用函数 public class PromotionEngine { // 策略注册表:type → 计算函数 private static final Map<String, Function<Order, Double>> STRATEGIES = new HashMap<>(); static { // 满减:满200减30 STRATEGIES.put("FULL_REDUCTION", order -> order.getOriginalPrice() >= 200 ? order.getOriginalPrice() - 30 : order.getOriginalPrice()); // 8折 STRATEGIES.put("PERCENT_OFF", order -> order.getOriginalPrice() * 0.8); // 秒杀 STRATEGIES.put("FLASH_SALE", order -> order.getFlashSalePrice()); } public static double apply(String type, Order order) { Function<Order, Double> strategy = STRATEGIES.getOrDefault(type, o -> o.getOriginalPrice()); // 默认原价 return strategy.apply(order); } }✅函数式策略的优势:
- 不用写接口、不用写类,一个 Lambda 搞定
- 代码量减少80%
- 适合简单策略(无状态、无外部依赖)
⚠️但也有局限:
- 策略逻辑复杂时(超过10行),Lambda 可读性下降
- 需要注入 Service 时(如查用户信息),Lambda 不方便
- 不方便做单元测试
建议:简单策略用 Lambda,复杂策略用接口+类。
七、策略模式在 JDK 中的应用
你以为策略模式很"高大上"?其实你天天在用:
7.1 Arrays.sort() 的 Comparator
String[] names = {"Charlie", "Alice", "Bob"}; // 策略1:按字母排序 Arrays.sort(names, Comparator.naturalOrder()); // 策略2:按字母倒序 Arrays.sort(names, Comparator.reverseOrder()); // 策略3:按长度排序 Arrays.sort(names, Comparator.comparingInt(String::length)); sort() 方法不关心你怎么排,它只调 Comparator.compare()。排序算法是固定的,比较策略是你传的。这就是策略模式。
7.2 ThreadFactory
// 线程池用 ThreadFactory 策略来创建线程 ExecutorService pool = Executors.newFixedThreadPool( 10, r -> { Thread t = new Thread(r); t.setName("my-pool-" + t.getId()); t.setDaemon(true); // 守护线程 return t; } );线程池不关心线程怎么创建,你传什么策略它就怎么创建。
八、软考高频考点
| 考点 | 内容 |
|---|---|
| 模式分类 | 行为型模式 |
| 核心意图 | 定义一系列算法,封装每个算法,使它们可以互相替换 |
| 三个角色 | Strategy(策略接口)、ConcreteStrategy(具体策略)、Context(上下文) |
| 核心原则 | 开闭原则(对扩展开放,对修改关闭) |
| 常见搭配 | 策略 + 工厂(Map注册表)、策略 + 模板方法(抽象基类) |
| 优点 | ① 避免多重条件语句 ② 符合开闭原则 ③ 算法可独立测试和复用 |
| 缺点 | ① 策略类数量增多 ② 客户端需了解所有策略 ③ 仅适用于算法变体场景 |
| 经典案例 | 排序的Comparator、支付方式选择、优惠策略引擎 |
| 易混淆 | 策略 vs 状态:策略是"主动选择",状态是"自动切换" |
🎯软考高频真题:
"某电商系统支持多种促销策略,要求新增促销方式时不修改已有代码。应采用()设计模式。" → 答案:** 策略模式** "策略模式的主要优点是()" → 答案:** 符合开闭原则,可以在不修改原有代码的情况下增加新算法**
九、策略 vs 状态:别搞混了
策略模式和状态模式长得很像(都是接口+多个实现),但本质完全不同:
| 维度 | 策略模式 | 状态模式(下篇预告) |
|---|---|---|
| 谁选择 | 客户端主动选择 | 对象内部自动切换 |
| 是否感知 | 客户端知道用的哪个策略 | 客户端不关心当前状态 |
| 各策略是否独立 | 完全独立,互不知道 | 状态之间知道彼此,负责切换 |
| 典型场景 | 支付方式、优惠算法 | 订单状态机、审批流程 |
💡一句话区分:
**策略模式 **:你去餐厅,主动选择吃川菜还是粤菜。 **状态模式 **:订单从"待支付"到"已发货",自动流转,你不用管。
十、总结
策略模式是行为型模式的第一块基石,也是最实用的模式之一。
📋策略模式核心要点:
- **问题 **:多种算法用 if-else 堆在一起,改一个影响全部
- **方案 **:每种算法封装成独立的策略类,通过接口统一调用
- **进阶 **:策略 + 工厂(Map 注册表),消灭选择逻辑
- **简化 **:Java 8 Lambda 适合简单策略
- **搭配 **:抽象基类 + 模板方法,共享通用逻辑
- **本质 **:客户端主动选择算法,运行时可切换
行为型模式的旅程,从策略开始。
接下来,小明还会遇到:
- 订单状态机(状态模式)
- 事件广播系统(观察者模式)
- 审批流程引擎(模板方法 + 责任链)
- ……
每一个,都是他从小白进阶到架构师的必经之路。
