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 画一张策略模式的结构图
下面这张图展示了策略模式的核心结构。我习惯用这种图来跟团队成员对齐思路:
实际重构中的“小技巧”
嗯,这里有几个我踩过的坑,分享给你:
- 策略的注册方式:我建议用 Map 来管理策略实例,而不是在 Context 里写一堆 if-else 来创建策略。Map 的方式更灵活,也更容易扩展。
- 策略的创建时机:如果策略对象很轻量,可以在初始化时全部创建好。如果策略对象很重(比如需要加载配置),可以考虑用工厂模式延迟创建。
- 策略的默认行为:一定要处理“找不到对应策略”的情况。我习惯返回一个默认策略(比如不打折),而不是抛异常。这样系统更健壮。
一个小建议:重构时不要一次性把所有条件都改成策略模式。先挑一个最常变化的分支下手,改完跑通测试,再逐步替换其他分支。步子迈大了容易扯着蛋。
总结
策略模式说白了就是“把 if-else 拆成多个类”。它不是什么高深的技术,但用好了能让代码干净很多。我个人习惯在项目初期就预留策略接口,哪怕当时只有一种策略。因为业务变化是迟早的事,提前留好扩展点,后面会省很多事。
记住:重构不是炫技,是为了让代码更容易理解和修改。策略模式只是工具,别为了用模式而用模式。