照着做完,你就能把一坨几百行、改一个分支要翻半天的心跳式 if-else,拆成「加一个类就多一种规则」的可维护结构,而且改完还能保证行为完全一致。
第一步:先承认它确实是 if-else 地狱
典型现场长这样:
public BigDecimal calc(String type, BigDecimal amount) {
if ("NORMAL".equals(type)) {
return amount;
} else if ("VIP".equals(type)) {
return amount.multiply(new BigDecimal("0.9"));
} else if ("SVIP".equals(type)) {
return amount.multiply(new BigDecimal("0.8"));
} else if ("COUPON".equals(type)) {
return amount.subtract(new BigDecimal("10"));
} else {
throw new IllegalArgumentException("未知类型: " + type);
}
}
判断标准很简单:分支数 ≥ 3,且半年内还在加,就该重构了。如果只有两个分支且永远不变,别动它——过度设计比 if-else 更贵。
第二步:抽出策略接口
先想清楚两件事:输入是什么、输出是什么。上面的例子输入是金额(其实 type 已经被拆出去了),输出是折后金额。
public interface PriceStrategy {
/** 该策略负责的类型,必须全局唯一 */
String type();
/** 计算折后价 */
BigDecimal calc(BigDecimal amount);
}
注意:接口里只放「变的部分」。如果所有实现都要校验金额非负,把它写在调用方或模板方法里,别复制到每个实现类。
第三步:一个分支一个类
把原代码的每个 else if 体原样搬过来,逻辑一个字都别改——重构和行为修改绝不能在同一提交里做。
public class NormalPriceStrategy implements PriceStrategy {
@Override public String type() { return "NORMAL"; }
@Override public BigDecimal calc(BigDecimal amount) { return amount; }
}
public class VipPriceStrategy implements PriceStrategy {
private static final BigDecimal RATE = new BigDecimal("0.9");
@Override public String type() { return "VIP"; }
@Override public BigDecimal calc(BigDecimal amount) {
return amount.multiply(RATE);
}
}
SVIP、COUPON 同理。此时每个类都短到一眼能看完,写单元测试也只需要 new 一个对象。
第四步:用 Map 建注册表,干掉 if-else
分支判断的本质是「按 key 找处理逻辑」,那用一个 Map 就够了。
public class PriceStrategyFactory {
private final Map<String, PriceStrategy> map = new ConcurrentHashMap<>();
public PriceStrategyFactory(List<PriceStrategy> strategies) {
for (PriceStrategy s : strategies) {
PriceStrategy old = map.put(s.type(), s);
if (old != null) {
throw new IllegalStateException("重复的策略 type: " + s.type());
}
}
}
public PriceStrategy get(String type) {
PriceStrategy s = map.get(type);
if (s == null) {
throw new IllegalArgumentException("未知类型: " + type);
}
return s;
}
}
调用处只剩一行:
return factory.get(type).calc(amount);
注意:注册时主动检查重复 key。用 Map 最大的风险就是两个类返回了同一个 type 字符串,静默覆盖后线上行为诡异,宁可启动就报错。
第五步:接 Spring 时让它自动收集
Spring 容器里,List<PriceStrategy> 会自动注入所有实现类,不用手写注册代码:
@Component
public class PriceStrategyFactory {
private final Map<String, PriceStrategy> map = new ConcurrentHashMap<>();
public PriceStrategyFactory(List<PriceStrategy> strategies) {
strategies.forEach(s -> map.put(s.type(), s));
}
// get 方法同上
}
不想用 Spring 的,就在工厂里手动 register(new VipPriceStrategy()),或者用枚举 + 抽象方法(分支极少时更轻)。
注意:如果某个策略需要事务、要发 MQ,务必让 Spring 管理它,别自己 new,否则 @Transactional 会静默失效——这是最常见的坑。
第六步:验证改造没改变行为
改造前先给旧方法补上测试,跑绿;改造后再跑一遍,两次都绿才算安全。
@ParameterizedTest
@CsvSource({"NORMAL,100,100", "VIP,100,90", "SVIP,100,80", "COUPON,100,90"})
void testCalc(String type, String in, String out) {
assertEquals(new BigDecimal(out),
factory.get(type).calc(new BigDecimal(in)));
}
COUPON 减 10 的逻辑、金额边界(0、负数、超大值)都要覆盖到,别只测 happy path。
小结
- 判断是否值得重构:分支 ≥ 3 且还会增长;只有两个固定分支就别动。
- 顺序:补测试 → 抽接口 → 原样搬实现类 → Map 注册 → 替换调用点。
type() 返回的 key 要唯一,注册时检测重复,宁可启动失败。
- 接 Spring 用
List<接口> 自动注入,注意别绕过容器 new 对象,否则事务失效。
- 最大收益不是代码好看,而是新增一种规则不用改老代码——这才是开闭原则的落地。