← 返回题目列表

策略模式如何消除大量 if-else?

高频 中等 第 8 / 26 题 更新于 2026/07/28
策略模式if-else代码重构开闭原则

简化版

策略模式通过把每个分支对应的算法封装成独立策略,再用工厂或注册表按类型选择策略,从而让主流程从“判断分支并执行”变成“获取策略并执行”。它不是完全消灭判断,而是把变化点集中管理。

详细版

大量 if-else 通常有两个问题:

  • 主流程和具体规则混在一起,代码越来越长;
  • 新增规则必须修改原方法,容易影响已有分支。

策略模式的重构步骤一般是:

  1. 抽象出统一策略接口;
  2. 为每个分支创建具体策略类;
  3. 用 Map、枚举、工厂或依赖注入容器维护类型到策略的映射;
  4. 主流程根据类型取出策略并调用统一方法。

例如:

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 的关键不是“没有判断”,而是“把变化的分支封装成策略,把选择集中到工厂或注册表”。主流程只负责拿策略、调策略,新增规则时少改老代码。