策略模式如何消除大量 if-else?
简化版
策略模式通过把每个分支对应的算法封装成独立策略,再用工厂或注册表按类型选择策略,从而让主流程从“判断分支并执行”变成“获取策略并执行”。它不是完全消灭判断,而是把变化点集中管理。
详细版
大量 if-else 通常有两个问题:
- 主流程和具体规则混在一起,代码越来越长;
- 新增规则必须修改原方法,容易影响已有分支。
策略模式的重构步骤一般是:
- 抽象出统一策略接口;
- 为每个分支创建具体策略类;
- 用 Map、枚举、工厂或依赖注入容器维护类型到策略的映射;
- 主流程根据类型取出策略并调用统一方法。
例如:
PaymentStrategy strategy = paymentStrategyFactory.get(payType);
strategy.pay(order);
这样主流程不再关心支付宝、微信、银行卡各自怎么支付,只关心拿到一个能执行支付的策略。
需要注意的是,策略模式通常会把选择逻辑集中到工厂或注册表中。如果选择规则本身很复杂,还可以继续拆成规则引擎、责任链或配置化机制。
完整版教学
一、先看 if-else 为什么会失控
很多业务代码最开始是这样的:
public BigDecimal calculate(String type, BigDecimal amount) {
if ("normal".equals(type)) {
return amount;
}
if ("vip".equals(type)) {
return amount.multiply(new BigDecimal("0.8"));
}
if ("coupon".equals(type)) {
return amount.subtract(new BigDecimal("50"));
}
throw new IllegalArgumentException("unsupported type");
}
这种写法在分支少时简单直接,但当规则增加到十几个、几十个时,会出现明显问题:
- 方法越来越长,阅读时要在分支里来回跳;
- 改一个分支可能误伤其他分支;
- 单元测试需要覆盖一个巨大的方法;
- 新增规则违反开闭原则;
- 规则复用困难。
策略模式重构的核心,是把每个分支变成一个可独立演进的对象。
二、第一步:提取稳定接口
先找到所有分支共同做的事情。
public interface DiscountStrategy {
String type();
BigDecimal calculate(BigDecimal amount);
}
这里 calculate 是执行行为,type 可以用于注册映射。也可以不把 type() 放在接口里,而是在外部配置映射关系。
接口设计要避免把具体分支细节塞进去。例如不要为了某个策略加一堆特殊参数,否则其他策略会被迫接收无意义数据。
三、第二步:每个分支一个策略
public class VipDiscountStrategy implements DiscountStrategy {
public String type() {
return "vip";
}
public BigDecimal calculate(BigDecimal amount) {
return amount.multiply(new BigDecimal("0.8"));
}
}
public class CouponDiscountStrategy implements DiscountStrategy {
public String type() {
return "coupon";
}
public BigDecimal calculate(BigDecimal amount) {
return amount.subtract(new BigDecimal("50"));
}
}
拆完后,每个策略类只处理自己的规则。新增一种折扣,不再需要打开原来的大方法,而是新增一个实现类。
四、第三步:用注册表替代分支选择
策略选择可以用 Map 完成:
public class DiscountStrategyFactory {
private final Map<String, DiscountStrategy> strategies;
public DiscountStrategyFactory(List<DiscountStrategy> strategyList) {
this.strategies = strategyList.stream()
.collect(Collectors.toMap(DiscountStrategy::type, s -> s));
}
public DiscountStrategy get(String type) {
DiscountStrategy strategy = strategies.get(type);
if (strategy == null) {
throw new IllegalArgumentException("unsupported discount type");
}
return strategy;
}
}
主流程变成:
DiscountStrategy strategy = factory.get(type);
return strategy.calculate(amount);
这里仍然有“找不到策略”的判断,但这个判断是兜底处理,不是业务分支爆炸。
五、策略模式不是万能替换器
不是所有 if-else 都值得改成策略模式。
适合改造的判断通常有这些特征:
- 每个分支代表独立算法或业务规则;
- 分支数量较多;
- 规则经常变化;
- 每个分支内部逻辑不止一两行;
- 需要单独测试或复用。
不适合的情况包括:
- 判断很少且不会变化;
- 分支只是简单赋值;
- 分支之间高度共享内部状态;
- 选择逻辑本身比执行逻辑复杂很多。
如果为了消除两行 if 创建十几个类,代码并不会更好维护。
六、把变化分支迁出主流程
原方法有 8 个支付分支,每新增 1 种都修改同一方法;注册表改造后新增策略通常只增加 1 个实现和 1 条注册。
type -> Map<key, Strategy> -> execute; unknown -> explicit fallback
这个推演把模式带来的收益和成本放在同一条调用链上。设计评审时既要确认结果正确,也要确认额外层次没有改变原有契约。
七、边界、代价与验证
| 检查维度 | 应确认的内容 |
|---|---|
| 正确性 | 目标不是追求零 if,而是让新增业务规则不再反复修改稳定主流程。 |
| 适用边界 | 分支若是校验顺序或简单阈值,不一定属于算法族;盲目拆类只会把复杂度从一个文件搬到多个文件。 |
| 测试证据 | 覆盖正常路径、异常路径、空值或未知类型,并断言调用次数与顺序 |
| 运行成本 | 记录额外对象、调用层数、延迟、线程或内存开销,不用模式名称代替测量 |
模式落地后要用单元测试验证独立职责,用集成测试验证对象装配和真实调用入口。涉及并发、远程或异步时,还要补充竞态、超时、重复执行与资源释放测试。
易错点:目标不是追求零 if,而是让新增业务规则不再反复修改稳定主流程。
八、常见误区与追问
- 误区:把 if-else 搬进一个巨大工厂就完成了开闭原则。 若工厂仍随每种策略增长而修改,变化点只是换了位置;注册机制才进一步隔离选择。
- 误区:用了策略模式,系统里就不该再出现任何条件判断。 策略消除的是反复扩张的业务算法分支;选择策略、校验输入和兜底仍可能需要有限判断。
- 误区:示例代码能运行,就代表模式边界设计正确。 能运行只证明一条路径,仍要检查异常、并发、顺序、生命周期以及新增实现时是否修改稳定代码。
- 追问:未知策略键应该怎么处理? 应显式拒绝、使用经过业务确认的默认策略,并记录监控,不能静默返回 null。
- 追问:策略对象可以保存本次请求的可变状态吗? 容器单例策略通常不应保存请求状态,应把订单、用户等数据作为参数传入,否则并发调用会相互污染。
- 追问:怎样验证这套设计没有改变原有语义? 先做基线行为测试,再对新增层验证返回值、异常类型、调用次数和顺序;性能敏感处还要比较改造前后的量化指标。
九、加强记忆
策略模式消除大量 if-else 的关键不是“没有判断”,而是“把变化的分支封装成策略,把选择集中到工厂或注册表”。主流程只负责拿策略、调策略,新增规则时少改老代码。