你写的 if-else 其实就是给自己埋雷——一个真实电商折扣系统的重构记录

你写的 if-else 其实就是给自己埋雷——一个真实电商折扣系统的重构记录 你写的 if-else 其实就是给自己埋雷——一个真实电商折扣系统的重构记录去年接手了一个电商项目的促销模块打开DiscountService的那一刻我脑子里只闪过一个念头这代码的作者是不是跟公司有仇。整个calculate方法400 行。没有夸张真的是 400 行。if-else 嵌套最深的地方有 5 层缩进已经把代码挤到了屏幕右半边。注释全部是// 这里加了个临时的判断后面再优化——注释的日期是两年前。还原一下案发现场java public BigDecimal calculate(BigDecimal price, int quantity, String discountType, String userLevel, boolean isNewUser, boolean isFlashSale, String couponCode, LocalDate orderDate) {BigDecimal total price.multiply(BigDecimal.valueOf(quantity)); if (FULL_REDUCTION.equals(discountType)) { if (total.compareTo(new BigDecimal(200)) 0) { total total.subtract(new BigDecimal(50)); if (VIP.equals(userLevel)) { total total.subtract(new BigDecimal(10)); } } else if (total.compareTo(new BigDecimal(100)) 0) { total total.subtract(new BigDecimal(15)); } } else if (PERCENTAGE.equals(discountType)) { if (isNewUser !isFlashSale) { total total.multiply(new BigDecimal(0.7)); } else if (isFlashSale) { total total.multiply(new BigDecimal(0.85)); } else { total total.multiply(new BigDecimal(0.9)); } } else if (COUPON.equals(discountType)) { // 这里又接了另一个 if-else 判断优惠券类型... if (BIRTHDAY.equals(couponCode)) { if (orderDate.getMonth() LocalDate.now().getMonth()) { total total.subtract(new BigDecimal(30)); } } else if (FESTIVAL.equals(couponCode)) { // ... 省略 20 行 } else { // 通用优惠券逻辑 } } else if (BUY_ONE_GET_ONE.equals(discountType)) { int payableCount (quantity 1) / 2; total price.multiply(BigDecimal.valueOf(payableCount)); } // 下面还有更多 else if... return total;} 每次运营想加一个活动就在这个 if-else 链里找一个合适的位置塞进去。代码评审的时候没人愿意细看因为光理解上下文就要花 10 分钟。测试就更别提了——要覆盖所有分支的组合测试用例数量是指数级增长的。第一步把每个分支变成一个策略类先抽策略接口。这不是什么高深的架构动作就是把每个 if 块变成一个独立的类java public interface DiscountStrategy { BigDecimal calculate(DiscountContext context); }Data Builder public class DiscountContext { private BigDecimal price; private int quantity; private String userLevel; private boolean newUser; private boolean flashSale; private String couponCode; private LocalDate orderDate; } DiscountContext把所有原来散乱在参数列表里的字段聚合成一个对象。这一步做得越早越好——把参数打包成上下文对象你会发现后续加字段不用改接口签名了调用方也不用改。然后每个策略实现java Component public class FullReductionStrategy implements DiscountStrategy {private static final BigDecimal TIER1_THRESHOLD new BigDecimal(200); private static final BigDecimal TIER1_REDUCTION new BigDecimal(50); private static final BigDecimal TIER2_THRESHOLD new BigDecimal(100); private static final BigDecimal TIER2_REDUCTION new BigDecimal(15); private static final BigDecimal VIP_EXTRA_REDUCTION new BigDecimal(10); Override public BigDecimal calculate(DiscountContext ctx) { BigDecimal total ctx.getPrice() .multiply(BigDecimal.valueOf(ctx.getQuantity())); if (total.compareTo(TIER1_THRESHOLD) 0) { total total.subtract(TIER1_REDUCTION); if (VIP.equals(ctx.getUserLevel())) { total total.subtract(VIP_EXTRA_REDUCTION); } } else if (total.compareTo(TIER2_THRESHOLD) 0) { total total.subtract(TIER2_REDUCTION); } return total; }}Component public class PercentageStrategy implements DiscountStrategy {Override public BigDecimal calculate(DiscountContext ctx) { BigDecimal total ctx.getPrice() .multiply(BigDecimal.valueOf(ctx.getQuantity())); BigDecimal ratio; if (ctx.isNewUser() !ctx.isFlashSale()) { ratio new BigDecimal(0.7); } else if (ctx.isFlashSale()) { ratio new BigDecimal(0.85); } else { ratio new BigDecimal(0.9); } return total.multiply(ratio); }} 每个类的职责是清楚的。改满减规则只改FullReductionStrategy不用担心把百分比折扣搞崩。第二步工厂 Map 消除策略选择逻辑有了一堆策略类原来那层 if-else 判断该用哪个策略也得干掉。最简单的方式是让每个策略声明自己负责什么类型注册到一个 Map 里java Component public class DiscountStrategyRegistry {private final MapString, DiscountStrategy strategyMap new HashMap(); Autowired public DiscountStrategyRegistry(ListDiscountStrategy strategies) { for (DiscountStrategy strategy : strategies) { // 每个策略实现类上打注解声明自己的类型 DiscountType annotation strategy.getClass() .getAnnotation(DiscountType.class); if (annotation ! null) { strategyMap.put(annotation.value(), strategy); } } } public DiscountStrategy getStrategy(String type) { DiscountStrategy strategy strategyMap.get(type); if (strategy null) { throw new IllegalArgumentException(Unknown discount type: type); } return strategy; }} 配上自定义注解java Target(ElementType.TYPE) Retention(RetentionPolicy.RUNTIME) public interface DiscountType { String value(); }DiscountType(FULL_REDUCTION) Component public class FullReductionStrategy implements DiscountStrategy { ... } 调用方变成一行java BigDecimal result strategyRegistry.getStrategy(discountType) .calculate(context);新加一个策略写一个新类打上DiscountType注解Spring 自动注入。不需要改工厂代码不需要改调用方代码开闭原则自动满足。策略模式的本质不是替换 if-else很多人把策略模式理解成消除 if-else 的语法糖这是本末倒置。策略模式解决的核心问题是让你的代码在面对变化时只改一个地方。if-else 的问题不是看起来丑而是当你需要加第 8 种折扣时你必须打开同一个calculate方法去改——这个方法的修改历史里可能有 20 个 commit任何一个改动都可能引入 bug。拆成策略类之后加新折扣 新建一个类不改任何旧代码。这才是真正的收益。但也要警惕一条不要为了消灭 if-else 而消灭 if-else。如果你的分支只有两三个而且分支逻辑非常稳定比如性别判断写个 if-else 完全够用。策略模式的开销是增加了类的数量和一个注册机制——对于频繁变动的逻辑这笔开销很值对于万年不变的两三个分支这是过度设计。业务复杂度超过策略模式的时候真实项目里还遇到过一个场景不同地区的促销规则不一样而且一个订单可能同时享受多个促销活动。比如北京区域满减 新人折扣 平台补贴三个策略叠加。但叠加不是简单的链式调用——有的策略互斥有的需要按优先级排序。这时候策略模式本身不够了需要引入责任链 组合规则引擎java Component public class CompositeDiscountEngine {private final ListDiscountStrategy strategies; private final RuleEngine ruleEngine; public BigDecimal calculate(DiscountContext ctx) { // 规则引擎判断哪些策略可用、优先级、互斥关系 ListDiscountStrategy activeStrategies ruleEngine .filter(strategies, ctx); BigDecimal result ctx.getPrice() .multiply(BigDecimal.valueOf(ctx.getQuantity())); for (DiscountStrategy strategy : activeStrategies) { result strategy.calculate(ctx.toBuilder() .price(result) .build()); } return result; }} 策略模式是单兵作战真实业务往往要协同——多个模式配合才是日常。我在做的一个小工具里用卡皮巴拉漫画的方式把这些模式的协同画成了故事比看干巴巴的架构图有意思。小程序「爪爪代码冒险记」就是干这个的——用漫画讲模式用关卡考你是否真的理解了。每个模式单独讲清楚之后下一个目标就是看多个模式怎么搭在一起干活。