28、重构与设计模式(一):用策略模式重构复杂条件逻辑

说实话,我见过太多代码里堆满了 if-else 和 switch-case。一开始还好,业务一复杂,条件分支能长到让你怀疑人生。今天我们就聊聊怎么用策略模式,把这些“面条式”的条件逻辑拆得干干净净。

先看一个“典型”的烂代码

假设你有个订单系统,要根据不同的用户等级计算折扣。我猜你大概率见过这样的代码:

public double calculateDiscount(String userLevel, double amount) {
    if ("VIP".equals(userLevel)) {
        return amount * 0.8;
    } else if ("GOLD".equals(userLevel)) {
        return amount * 0.85;
    } else if ("SILVER".equals(userLevel)) {
        return amount * 0.9;
    } else if ("NORMAL".equals(userLevel)) {
        return amount * 0.95;
    } else {
        return amount;
    }
}

这段代码有什么问题?

  • 每加一种用户等级,就要改这个函数。改着改着就改出 bug 了。
  • 条件分支越来越多,测试用例也跟着膨胀。
  • 不同等级的逻辑耦合在一起,想单独测某个等级?你得把所有条件都 mock 一遍。

我在项目中遇到过类似的情况,当时一个订单模块的折扣逻辑有 12 个分支。每次产品经理说“再加一种会员等级”,我就头皮发麻。后来我下定决心重构,用的就是策略模式。

策略模式:把“怎么做”拆出去

策略模式的核心思想很简单:把算法(或行为)封装成独立的类,让它们可以互相替换。说白了,就是把“做什么”和“怎么做”分开。

拿上面的折扣计算来说,每种用户等级对应一种“折扣策略”。我们把这些策略抽出来:

// 策略接口
public interface DiscountStrategy {
    double calculate(double amount);
}

// 具体策略:VIP
public class VipDiscount implements DiscountStrategy {
    @Override
    public double calculate(double amount) {
        return amount * 0.8;
    }
}

// 具体策略:GOLD
public class GoldDiscount implements DiscountStrategy {
    @Override
    public double calculate(double amount) {
        return amount * 0.85;
    }
}

// 具体策略:SILVER
public class SilverDiscount implements DiscountStrategy {
    @Override
    public double calculate(double amount) {
        return amount * 0.9;
    }
}

// 具体策略:NORMAL
public class NormalDiscount implements DiscountStrategy {
    @Override
    public double calculate(double amount) {
        return amount * 0.95;
    }
}

然后,原来的条件逻辑就变成了一个“策略选择器”:

public class DiscountCalculator {
    private Map<String, DiscountStrategy> strategyMap = new HashMap<>();

    public DiscountCalculator() {
        strategyMap.put("VIP", new VipDiscount());
        strategyMap.put("GOLD", new GoldDiscount());
        strategyMap.put("SILVER", new SilverDiscount());
        strategyMap.put("NORMAL", new NormalDiscount());
    }

    public double calculate(String userLevel, double amount) {
        DiscountStrategy strategy = strategyMap.get(userLevel);
        if (strategy == null) {
            return amount; // 默认不打折
        }
        return strategy.calculate(amount);
    }
}

你看,原来的 if-else 消失了。现在要加一种“PLATINUM”等级?只需要新增一个 PlatinumDiscount 类,然后在构造器里注册一下就行。原来的代码一行都不用改。

核心变化:从“修改已有代码”变成了“扩展新代码”。这就是开闭原则(OCP)的体现。

为什么策略模式能解决“条件爆炸”?

你想想看,条件逻辑的本质是什么?是“根据某个输入,选择不同的行为”。策略模式把“选择”和“行为”拆开了:

  • 选择逻辑:由调用方或一个工厂类负责。
  • 行为逻辑:由各个策略类独立实现。

这样一来,每个策略类只关心自己的计算逻辑,测试起来也简单。我曾经重构过一个支付模块,里面根据支付方式(微信、支付宝、银行卡、积分……)有 7 个分支。重构后,每个支付策略单独一个类,测试覆盖率从 60% 直接升到 95%。

我的习惯:如果条件分支超过 3 个,我就会考虑用策略模式。3 个以内,if-else 还能接受;超过 3 个,代码的可读性和可维护性就开始直线下降。

避坑指南:策略模式不是银弹

我曾经犯过一个错误:把策略模式用在了“条件判断本身就很稳定”的场景里。比如一个工具函数,只有两种固定行为,而且几乎不会变。这种情况下,用策略模式反而增加了代码的复杂度。

策略模式适合的场景:

  • 条件分支经常变化(新增、删除、修改)。
  • 每个分支的逻辑都比较复杂,值得单独封装。
  • 你希望每个分支都能独立测试。

不适合的场景:

  • 分支很少(2-3 个)且几乎不变。
  • 分支逻辑极其简单(比如一行 return)。
  • 团队对设计模式不熟悉,强行引入反而增加沟通成本。

注意:策略模式会增加类的数量。如果你有 20 种策略,就要创建 20 个类。这时候可以考虑用枚举 + 函数式接口来简化(Java 8 以后)。

用 SVG 画一张策略模式的结构图

下面这张图展示了策略模式的核心结构。我习惯用这种图来跟团队成员对齐思路:

策略模式结构图 Context (折扣计算器) <<interface>> DiscountStrategy VipDiscount (VIP 策略) GoldDiscount (GOLD 策略) SilverDiscount (SILVER 策略) NormalDiscount (NORMAL 策略) Context 持有策略接口,具体策略实现算法,两者通过组合关联

实际重构中的“小技巧”

嗯,这里有几个我踩过的坑,分享给你:

  1. 策略的注册方式:我建议用 Map 来管理策略实例,而不是在 Context 里写一堆 if-else 来创建策略。Map 的方式更灵活,也更容易扩展。
  2. 策略的创建时机:如果策略对象很轻量,可以在初始化时全部创建好。如果策略对象很重(比如需要加载配置),可以考虑用工厂模式延迟创建。
  3. 策略的默认行为:一定要处理“找不到对应策略”的情况。我习惯返回一个默认策略(比如不打折),而不是抛异常。这样系统更健壮。

一个小建议:重构时不要一次性把所有条件都改成策略模式。先挑一个最常变化的分支下手,改完跑通测试,再逐步替换其他分支。步子迈大了容易扯着蛋。

总结

策略模式说白了就是“把 if-else 拆成多个类”。它不是什么高深的技术,但用好了能让代码干净很多。我个人习惯在项目初期就预留策略接口,哪怕当时只有一种策略。因为业务变化是迟早的事,提前留好扩展点,后面会省很多事。

记住:重构不是炫技,是为了让代码更容易理解和修改。策略模式只是工具,别为了用模式而用模式。

公众号:蓝海资料掘金营,微信deep3321