设计模式预计阅读 20 分钟7953 字1681 次浏览

设计模式修炼之路 ⑮ 策略模式——if-else 的解药

一个分支一个类、工厂加 Map 连选择处都不写 if-else、Spring 自动收集、线程池拒绝策略

目录

行为型第二站。

上一篇结尾立了个 flag:你代码里那个嵌套五层、每来一个新需求就得多写一个 else if 的方法,读完这篇就有解了。

解药就是它——策略模式,GoF 23 里出场率最高的模式之一,江湖人称"if-else 杀手"。

一、策略模式是什么

先看 GoF 的原始定义:

Define a family of algorithms, encapsulate each one, and make them interchangeable. (定义一族算法,把每一个封装起来,让它们可以互相替换。)

翻译成人话:if-else 里的每个分支,抽出来变成一个独立的类,全都实现同一个接口。要用哪个,传哪个。

就这么多,没有更玄的东西了。

三个角色:

角色 干什么 本文对应
Strategy(策略接口) 统一契约,所有算法长一个样 PromotionStrategy
ConcreteStrategy(具体策略) 一个分支一个类 满减、打折、立减、N元购
Context(环境类) 持有策略,把活儿委托出去 PromotionContext

类比:导航 App 里的路线偏好——"时间最短""距离最短""避开收费""不走高速"。导航的骨架(定位、算路、播报)一毫米不动,换的只是"怎么算路"这一个策略。你点哪个按钮,App 就给 Context 塞哪个策略。

和上一篇模板模式对比着记:模板换的是流程里的某几步(填空题),策略换的是整个算法(换试卷)。模板靠继承,策略靠组合——这正是 ⑭ 结尾说的"这个时代的答案"。

二、优点与缺点

优点:

  1. if-else 消失,开闭原则达成。新活动上线 = 加一个类 + 注册一行,老方法从此不动。
  2. 每个算法独立演进、独立测试。满减要升级成阶梯满减,只改 FullReductionStrategy 一个文件,测试也只测这一个类,不用担心牵连打折。
  3. 运行时可换。策略是个对象,对象可以换——白天满减、晚上八点切 N元购,改个字段的事。
  4. 用组合替代继承。不烧单继承名额,想叠几个叠几个。

缺点:

  1. 类膨胀。五种活动五个类,活动多了目录里一片 Strategy,有人管这叫"类爆炸"。
  2. 策略暴露给调用方。客户端得知道有哪几种策略、区别是什么,才能选。选择困难症从服务端转移到了调用端——所以本文第四节还要请工厂来救场。
  3. 简单场景是杀鸡用牛刀。就两个固定分支、三年没变过?老老实实 if-else,可读性反而更好。模式是给"变化"买的保险,不变化的代码不需要保险。

三、什么时候用

判断标准就一条:这些分支会不会增长?

  • 业务规则会增长:促销活动、支付渠道(微信/支付宝/银联)、会员等级价、物流报价
  • 算法可替换:压缩(GZIP/ZIP/deflate)、加密(AES/RSA/SM4)、限流(计数器/滑动窗口/令牌桶/漏桶)
  • 同一动作多种实现:导出 Excel/PDF/CSV、发送短信/邮件/钉钉

反过来三个字:别硬套。两个稳定分支硬上策略模式,只会让读代码的人骂娘。

四、实现方式:把 if-else 掰碎重装

4.1 反面教材:促销计算巨无霸

电商场景,一个方法算促销价。运营配了什么活动,就传什么类型进来:

public class BadPromotionCalculator {

    public double calculate(String promotionType, double amount) {
        if ("满减".equals(promotionType)) {
            if (amount >= 300) {
                System.out.println("[满减] 满 300 减 50");
                return amount - 50;
            }
            System.out.println("[满减] 未满 300,不参与");
            return amount;
        } else if ("打折".equals(promotionType)) {
            System.out.println("[打折] 全场 8 折");
            return amount * 0.8;
        } else if ("立减".equals(promotionType)) {
            System.out.println("[立减] 无门槛立减 10 元");
            return amount - 10;
        } else if ("N元购".equals(promotionType)) {
            System.out.println("[N元购] 一口价 9.9 元");
            return 9.9;
        } else {
            System.out.println("[无活动] 原价购买");
            return amount;
        }
        // 运营说下个月要上"买一送一"和"满额包邮"……
        // 这个方法已经 50 行了,圈复杂度快爆表,改一处手抖全崩
    }
}

现在四个活动还勉强看得过去。但是——

  • 下个月运营要上"买一送一",得改这里;
  • 大后天要上"满额包邮",还得改这里;
  • 满减要升级成阶梯满减(满 300 减 50、满 600 减 150),继续改这里。

三个月后这个方法两百行,圈复杂度爆表,每次发版回归测试要覆盖所有历史分支。它违反开闭原则的方式和 ⑭ 的反面教材一模一样:所有变化都往一个方法里堆。

4.2 正解:一个分支一个类

第一步,把分支的共性提炼成接口。所有活动干的是同一件事——"给定金额,算出应付":

public interface PromotionStrategy {

    double calculate(double amount);
}

第二步,每个分支一个类:

public class FullReductionStrategy implements PromotionStrategy {

    @Override
    public double calculate(double amount) {
        if (amount >= 300) {
            System.out.println("  [满减] 满 300 减 50");
            return amount - 50;
        }
        System.out.println("  [满减] 未满 300,不参与");
        return amount;
    }
}

public class DiscountStrategy implements PromotionStrategy {

    @Override
    public double calculate(double amount) {
        System.out.println("  [打折] 全场 8 折");
        return amount * 0.8;
    }
}

public class InstantReductionStrategy implements PromotionStrategy {

    @Override
    public double calculate(double amount) {
        System.out.println("  [立减] 无门槛立减 10 元");
        return amount - 10;
    }
}

public class FixedPriceStrategy implements PromotionStrategy {

    @Override
    public double calculate(double amount) {
        System.out.println("  [N元购] 一口价 9.9 元");
        return 9.9;
    }
}

第三步,Context 拿着策略干活:

public class PromotionContext {

    private final PromotionStrategy strategy;

    public PromotionContext(PromotionStrategy strategy) {
        this.strategy = strategy;
    }

    public double checkout(double amount) {
        return strategy.calculate(amount);
    }
}

测试类让反面教材和正解同台对比:

public class StrategyTest {

    public static void main(String[] args) {
        System.out.println("========== 反面教材:if-else 巨无霸 ==========");
        BadPromotionCalculator bad = new BadPromotionCalculator();
        System.out.println("满减 350 元 -> " + bad.calculate("满减", 350));
        System.out.println("打折 350 元 -> " + bad.calculate("打折", 350));
        System.out.println("立减 350 元 -> " + bad.calculate("立减", 350));
        System.out.println("N元购 350 元 -> " + bad.calculate("N元购", 350));
        System.out.println("周年庆 350 元 -> " + bad.calculate("周年庆", 350));

        System.out.println();
        System.out.println("========== 正解一:显式传入策略 ==========");
        PromotionContext context = new PromotionContext(new FullReductionStrategy());
        System.out.println("满减 350 元 -> " + context.checkout(350));
        System.out.println("满减 250 元 -> " + context.checkout(250));
        context = new PromotionContext(new DiscountStrategy());
        System.out.println("打折 350 元 -> " + context.checkout(350));
        context = new PromotionContext(new FixedPriceStrategy());
        System.out.println("N元购 350 元 -> " + context.checkout(350));

        System.out.println();
        System.out.println("========== 正解二:工厂 + Map,选择处也不再 if-else ==========");
        System.out.println("满减 350 元 -> " + new PromotionContext(PromotionStrategyFactory.get("满减")).checkout(350));
        System.out.println("立减 350 元 -> " + new PromotionContext(PromotionStrategyFactory.get("立减")).checkout(350));
        System.out.println("周年庆(没配,兜底) -> " + new PromotionContext(PromotionStrategyFactory.get("周年庆")).checkout(350));
    }
}

运行输出:

========== 反面教材:if-else 巨无霸 ==========
[满减] 满 300 减 50
满减 350 元 -> 300.0
[打折] 全场 8 折
打折 350 元 -> 280.0
[立减] 无门槛立减 10 元
立减 350 元 -> 340.0
[N元购] 一口价 9.9 元
N元购 350 元 -> 9.9
[无活动] 原价购买
周年庆 350 元 -> 350.0

========== 正解一:显式传入策略 ==========
  [满减] 满 300 减 50
满减 350 元 -> 300.0
  [满减] 未满 300,不参与
满减 250 元 -> 250.0
  [打折] 全场 8 折
打折 350 元 -> 280.0
  [N元购] 一口价 9.9 元
N元购 350 元 -> 9.9

========== 正解二:工厂 + Map,选择处也不再 if-else ==========
  [满减] 满 300 减 50
满减 350 元 -> 300.0
  [立减] 无门槛立减 10 元
立减 350 元 -> 340.0
  [无活动] 原价购买
周年庆(没配,兜底) -> 350.0

4.3 三处细节咂摸一下

细节一:满减 250 元走的是策略自己的内部逻辑。"未满 300 不参与"这种判断被完整封装进了 FullReductionStrategy,外界无感知。策略的粒度是"整个算法",算法内部再复杂也只污染它自己一个类。

**细节二:反面教材的 else 兜底,正解里是 NoPromotionStrategy。**那个 else return amount 其实是一个隐藏策略——"无活动"。把它也变成类之后,工厂查不到任何活动时返回它,调用方永远拿到一个能干活的策略,空指针从根上被消灭了。这个手法有个正式的名字——空对象模式,㉓ 会专门讲,这里先混个脸熟。

**细节三:测试里"周年庆"这个根本不存在的活动类型,两边都正常兜底返回原价。**反面教材靠 else,正解靠工厂默认值——但正解加新活动不用改一行旧代码,这才是本质区别。

4.4 工厂救场:选择处也不写 if-else

正解一还有个尾巴:调用方 new PromotionContext(new FullReductionStrategy()),还是得认识所有具体策略类——第二节说的"策略暴露"缺点没解决。而且真实项目里,活动类型是运营配置的字符串,从数据库里来。怎么办?工厂 + Map:

public class PromotionStrategyFactory {

    private static final Map<String, PromotionStrategy> STRATEGIES = new HashMap<>();

    static {
        STRATEGIES.put("满减", new FullReductionStrategy());
        STRATEGIES.put("打折", new DiscountStrategy());
        STRATEGIES.put("立减", new InstantReductionStrategy());
        STRATEGIES.put("N元购", new FixedPriceStrategy());
        STRATEGIES.put("无活动", new NoPromotionStrategy());
    }

    private PromotionStrategyFactory() {
    }

    public static PromotionStrategy get(String promotionType) {
        PromotionStrategy strategy = STRATEGIES.get(promotionType);
        return strategy != null ? strategy : STRATEGIES.get("无活动");
    }
}

Map.get() 一步到位,选择处一个 if-else 都没有。新活动上线 = 写一个类 + static 块里加一行注册。

4.5 Spring 时代的打开方式

上面的 static 注册块,在 Spring 项目里有更优雅的替身——容器自动收集:

@Component
public class PromotionStrategyHolder {

    private final Map<String, PromotionStrategy> strategyMap;

    public PromotionStrategyHolder(List<PromotionStrategy> strategies) {
        this.strategyMap = strategies.stream()
                .collect(Collectors.toMap(PromotionStrategy::type, Function.identity()));
    }

    public PromotionStrategy get(String type) {
        return strategyMap.getOrDefault(type, strategyMap.get("无活动"));
    }
}

构造器参数是 List<PromotionStrategy>——Spring 会把容器里所有实现了这个接口的 bean 打包成 List 注进来。给接口加一个 String type() 方法返回活动编码,再让 NoPromotionStrategy 的 type() 返回"无活动",stream 一把转成 Map。

从此新活动上线 = 写一个带 @Component 的类,连注册那一行都省了。策略模式和 Spring 的自动装配,堪称天作之合。

五、在框架里找它

**ThreadPoolExecutor 的拒绝策略。**线程池满了之后怎么办?JDK 给了四种打法:AbortPolicy(抛异常)、CallerRunsPolicy(调用者自己跑)、DiscardPolicy(悄悄丢弃)、DiscardOldestPolicy(丢最老的再试)——四个类,全实现 RejectedExecutionHandler 接口,构造线程池时传哪个就是哪个:

ThreadPoolExecutor executor = new ThreadPoolExecutor(
        4, 8, 60, TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(10),
        new ThreadPoolExecutor.CallerRunsPolicy());

想把"抛异常"换成"降级到调用者执行"?换个构造参数的事。这就是策略模式的运行时可换。

**Comparator。**你每天都在用策略模式而不自知:

list.sort(Comparator.comparing(Order::getAmount));            // 按金额升序
list.sort(Comparator.comparing(Order::getAmount).reversed()); // 按金额降序
list.sort(Comparator.comparing(Order::getCreateTime));        // 按时间排序

sort 骨架是 TimSort,不动;比大小的规则是策略,随便换。JDK 把策略模式做成了语言级语法糖——Lambda 本质上就是"可以传来传去的轻量策略"。

**Spring 的 InstantiationStrategy。**Bean 怎么实例化?SimpleInstantiationStrategy(反射直调构造器)和 CglibSubclassingInstantiationStrategy(CGLIB 生成子类再实例化,解决方法注入)两个策略来回切。和 ⑬ 代理模式那篇里的 CGLIB 又碰上了。

六、策略的黄金搭档

策略模式很少单打独斗,它有两个固定搭档:

策略 + 工厂:本文 4.4 已示范。工厂负责"造和选",策略负责"干"。这一对组合是面试手写题的钉子户——"用策略模式 + 工厂模式重构这段 if-else",出现频率高到值得单独准备。

策略 + 空对象:NoPromotionStrategy 那个兜底位。与其让调用方到处判空,不如给"没有"也造一个行为正常的类。㉓ 见。

最后泼一盆冷水:if-else 不是原罪。条件分支是计算机科学最伟大的发明之一,罪过只在于"会增长的分支"挤在一个方法里。策略模式不是消灭 if-else,而是把每个 if 背后的逻辑送进各自的类,让变化的影响半径从整个方法缩到一个类。衡量要不要重构,就看这棵 if-else 树半年内长了几片新叶子。

写在最后

策略模式一句话总结:一族可替换的算法,一个接口统帅,一个类一支兵,Context 指哪打哪。

回顾它和前几篇的关系:⑭ 模板模式管"流程里哪几步变化"(填空),⑮ 策略模式管"整个算法换掉"(换人)。一个靠继承,一个靠组合——行为型双雄,就此到齐了一半。

下一篇 ⑯ 观察者模式——策略解决的是"该用哪个算法",观察者解决的是"该通知谁"。订单支付成功后要发短信、加积分、扣库存、通知物流……你会怎么写?在 OrderService 里依次调四个方法?那每接一个新下游,下单代码就得改一次。一对多依赖、事件订阅与广播——观察者模式,下一篇见。

花

花开空白

开发者 · 西安

相关文章

评论(0)

还没有评论,来抢沙发吧

发表评论