策略模式
策略模式用于把一组可替换的算法或业务处理逻辑封装起来,让调用方在运行时选择不同策略。它常用于支付方式、折扣规则、文件解析、消息发送、导出格式等场景。
为什么需要策略模式
如果所有业务差异都写在一个方法里,会出现大量 if/else。
java
public BigDecimal calculate(String memberType, BigDecimal price) {
if ("VIP".equals(memberType)) {
return price.multiply(new BigDecimal("0.8"));
}
if ("SVIP".equals(memberType)) {
return price.multiply(new BigDecimal("0.7"));
}
if ("NEW_USER".equals(memberType)) {
return price.subtract(new BigDecimal("10"));
}
return price;
}问题:
- 新增规则要改老代码。
- 一个方法越来越长。
- 不同规则互相影响,测试困难。
- 不符合开闭原则。
策略模式把每种规则拆成独立类:
mermaid
flowchart TD
A["DiscountService"] --> B["StrategyFactory"]
B --> C{"memberType"}
C -- "VIP" --> D["VipDiscountStrategy"]
C -- "SVIP" --> E["SvipDiscountStrategy"]
C -- "NEW_USER" --> F["NewUserDiscountStrategy"]基本结构
mermaid
flowchart TD
A["业务上下文 Context"] --> B["策略接口 Strategy"]
B --> C["策略实现 A"]
B --> D["策略实现 B"]
B --> E["策略实现 C"]角色:
| 角色 | 作用 |
|---|---|
| Strategy | 定义统一行为接口 |
| ConcreteStrategy | 每个具体策略实现自己的算法 |
| Context/Factory | 根据业务条件选择策略并调用 |
策略模式的原理
策略模式把“选择规则”和“执行规则”拆开:
- 调用方只提出业务类型,例如
VIP、WECHAT、EXCEL。 - 上下文或工厂根据类型找到一个策略对象。
- 业务真正变化的算法写在策略对象中。
- 调用方拿到统一结果,不关心内部算法。
mermaid
flowchart TD
A["业务请求"] --> B["提取策略 key"]
B --> C["策略注册表 Map"]
C --> D{"找到策略"}
D -- "否" --> E["默认策略或明确异常"]
D -- "是" --> F["执行对应策略"]
F --> G["返回统一结果"]它解决的不是“代码里不能有 if”,而是当 if 的每个分支都代表一套会独立演进的业务规则时,把这些规则从一个大方法里拆出去。少量、稳定、非常简单的判断没有必要强行使用策略模式。
Java Demo
策略接口:
java
public interface DiscountStrategy {
String type();
BigDecimal calculate(BigDecimal price);
}VIP 策略:
java
@Component
public class VipDiscountStrategy implements DiscountStrategy {
@Override
public String type() {
return "VIP";
}
@Override
public BigDecimal calculate(BigDecimal price) {
return price.multiply(new BigDecimal("0.8"));
}
}新人策略:
java
@Component
public class NewUserDiscountStrategy implements DiscountStrategy {
@Override
public String type() {
return "NEW_USER";
}
@Override
public BigDecimal calculate(BigDecimal price) {
return price.subtract(new BigDecimal("10")).max(BigDecimal.ZERO);
}
}策略工厂:
java
@Component
public class DiscountStrategyFactory {
private final Map<String, DiscountStrategy> strategyMap;
public DiscountStrategyFactory(List<DiscountStrategy> strategies) {
this.strategyMap = strategies.stream()
.collect(Collectors.toMap(DiscountStrategy::type, strategy -> strategy));
}
public DiscountStrategy get(String type) {
DiscountStrategy strategy = strategyMap.get(type);
if (strategy == null) {
throw new IllegalArgumentException("不支持的会员类型: " + type);
}
return strategy;
}
}使用:
java
@Service
public class DiscountService {
private final DiscountStrategyFactory strategyFactory;
public DiscountService(DiscountStrategyFactory strategyFactory) {
this.strategyFactory = strategyFactory;
}
public BigDecimal calculate(String memberType, BigDecimal price) {
return strategyFactory.get(memberType).calculate(price);
}
}策略选择流程
mermaid
flowchart TD
A["接收业务类型"] --> B["从 Map 查找策略"]
B --> C{"策略是否存在"}
C -- "否" --> D["抛出明确异常或走默认策略"]
C -- "是" --> E["执行策略"]
E --> F["返回结果"]商业场景:消息通知策略
商业系统里经常需要发送通知:短信、邮件、站内信、企业微信、钉钉。通知这件事的目标相同,都是“把消息发给用户”,但不同渠道的参数、限流、失败处理完全不同。
如果写成一个方法:
java
public void send(String channel, Message message) {
if ("SMS".equals(channel)) {
// 调短信供应商
} else if ("EMAIL".equals(channel)) {
// 调邮件服务器
} else if ("WECHAT_WORK".equals(channel)) {
// 调企业微信
}
}后续会出现这些问题:
| 问题 | 后果 |
|---|---|
| 每个渠道参数不同 | 一个方法里到处判断和转换 |
| 失败处理不同 | 短信要换供应商,邮件要重试,站内信要落库 |
| 发送频率不同 | 限流规则互相污染 |
| 新增渠道频繁 | 老方法不断被修改,回归风险高 |
使用策略模式后,每个渠道一个策略:
mermaid
flowchart TD
A["通知服务"] --> B["NotificationStrategyFactory"]
B --> C{"channel"}
C -- "SMS" --> D["SmsNotificationStrategy"]
C -- "EMAIL" --> E["EmailNotificationStrategy"]
C -- "APP" --> F["AppNotificationStrategy"]
D --> G["发送结果"]
E --> G
F --> G策略接口:
java
public interface NotificationStrategy {
String channel();
SendResult send(Message message);
}短信策略:
java
@Component
public class SmsNotificationStrategy implements NotificationStrategy {
@Override
public String channel() {
return "SMS";
}
@Override
public SendResult send(Message message) {
// 真实项目中会做模板参数、签名、供应商调用、响应码转换
System.out.println("send sms: " + message.getContent());
return SendResult.success();
}
}邮件策略:
java
@Component
public class EmailNotificationStrategy implements NotificationStrategy {
@Override
public String channel() {
return "EMAIL";
}
@Override
public SendResult send(Message message) {
System.out.println("send email: " + message.getContent());
return SendResult.success();
}
}策略工厂:
java
@Component
public class NotificationStrategyFactory {
private final Map<String, NotificationStrategy> strategyMap;
public NotificationStrategyFactory(List<NotificationStrategy> strategies) {
this.strategyMap = strategies.stream()
.collect(Collectors.toMap(NotificationStrategy::channel, strategy -> strategy));
}
public NotificationStrategy getRequired(String channel) {
NotificationStrategy strategy = strategyMap.get(channel);
if (strategy == null) {
throw new IllegalArgumentException("不支持的通知渠道: " + channel);
}
return strategy;
}
}业务服务:
java
@Service
public class NotificationService {
private final NotificationStrategyFactory strategyFactory;
public NotificationService(NotificationStrategyFactory strategyFactory) {
this.strategyFactory = strategyFactory;
}
public SendResult send(String channel, Message message) {
return strategyFactory.getRequired(channel).send(message);
}
}新增钉钉通知时,只新增 DingTalkNotificationStrategy,不改通知服务主流程。
策略模式要不要配默认策略
这取决于业务风险:
| 场景 | 建议 | 原因 |
|---|---|---|
| 支付、扣款、权限 | 不建议默认策略,找不到就失败 | 错用默认策略会产生资金或安全问题 |
| 折扣计算 | 可以有默认原价策略 | 找不到优惠规则时按原价比较安全 |
| 通知发送 | 看业务要求 | 重要通知应该失败告警,营销通知可降级 |
| 文件解析 | 不建议默认策略 | 解析错格式会造成数据污染 |
策略选择失败时不要默默返回 null,要么抛出明确异常,要么走业务上明确允许的默认策略。
使用边界
适合使用:
- 确实存在多种可替换规则。
- 规则会持续新增。
- 每种规则相对独立。
- 调用方只关心统一结果。
不适合使用:
- 只有一两个简单判断。
- 规则不会变化。
- 策略之间强依赖执行顺序。
- 抽象后反而让代码更难读。
和工厂模式区别
| 对比 | 策略模式 | 工厂模式 |
|---|---|---|
| 关注点 | 行为怎么切换 | 对象怎么创建或获取 |
| 主要目的 | 隔离算法或业务规则 | 封装创建逻辑 |
| 实际组合 | 工厂返回某个策略 | 策略执行业务 |
滥用风险
| 风险 | 后果 |
|---|---|
| 规则很少也拆策略 | 类数量增加,阅读成本变高 |
| 策略参数过于通用 | 变成 Map/Object 到处强转 |
| 策略里互相调用 | 失去独立性,调试困难 |
| 选择逻辑散落各处 | 策略本身解耦了,选择过程又耦合 |
生产排查方法
策略模式的排查重点是:选错了策略,还是策略内部执行失败。
mermaid
flowchart TD
A["业务结果异常"] --> B["检查策略 key"]
B --> C{"key 是否正确"}
C -- "否" --> D["检查入参、枚举转换、配置映射"]
C -- "是" --> E["检查工厂注册表"]
E --> F{"策略是否存在"}
F -- "否" --> G["检查 Bean 扫描和 channel 重复"]
F -- "是" --> H["进入具体策略日志"]
H --> I["检查外部依赖、参数、异常处理"]建议日志至少包含:
| 字段 | 说明 |
|---|---|
strategyKey | 本次选择的策略 key |
strategyClass | 实际执行的策略类 |
businessId | 订单号、用户 ID、任务 ID 等业务标识 |
costMs | 策略执行耗时 |
resultCode | 外部服务或内部规则返回码 |
面试标准回答
text
策略模式适合一件事有多种做法,并且这些做法会独立变化的场景。它把每种算法或业务规则封装成独立策略,调用方根据类型选择策略并执行统一接口。比如通知系统中短信、邮件、站内信都实现 NotificationStrategy,新增钉钉通知时只新增策略类,不改通知主流程。它的优点是减少大段 if/else、隔离变化、方便单测;缺点是类数量增加,策略选择规则必须集中管理,不能让选择逻辑散落各处。追问补充:
| 追问 | 回答方向 |
|---|---|
| 策略模式是不是为了消灭 if/else | 不是。它是为了隔离会持续变化的业务规则,简单稳定判断不用强行抽策略 |
| 策略和工厂区别 | 工厂负责拿到对象,策略负责执行业务算法,项目中常一起使用 |
| 怎么处理找不到策略 | 高风险业务抛异常,低风险且业务允许时使用默认策略 |
| 策略太多怎么管理 | 统一 key、统一注册、统一日志,必要时按业务域拆包 |
小结
策略模式适合“同一件事有多种做法”的场景。它的价值是隔离变化,让新增规则尽量变成新增类,而不是修改一个越来越长的方法。但如果变化点不稳定或逻辑很简单,直接写清楚比提前抽象更好。
