Skip to content

用模式重构:从 if-else 地狱到可扩展设计

本文是设计模式系统学习系列的 L3 实战篇。前置:策略与模板方法责任链与命令。 延伸阅读:Spring 生产问题排查

先看一段代码,它能让你难受

下面是一个电商订单创建方法。300 行,但核心逻辑就这些——问题在于它们全部挤在一个方法里:

java
public String createOrder(OrderRequest req) {
    // 1. 参数校验
    if (req.getUserId() == null || req.getUserId() <= 0) throw new IllegalArgumentException("用户非法");
    if (req.getItems() == null || req.getItems().isEmpty()) throw new IllegalArgumentException("商品列表为空");
    if (req.getTotalAmount() == null || req.getTotalAmount().compareTo(BigDecimal.ZERO) <= 0) throw new IllegalArgumentException("金额非法");
    if (req.getAddress() == null || req.getAddress().isBlank()) throw new IllegalArgumentException("地址不能为空");
    if (req.getPaymentMethod() == null) throw new IllegalArgumentException("支付方式不能为空");
    // 黑名单校验
    User user = userService.findById(req.getUserId());
    if (user == null) throw new IllegalArgumentException("用户不存在");
    if (user.isBlacklisted()) throw new IllegalArgumentException("用户已被拉黑");
    // 库存校验
    for (Item item : req.getItems()) {
        Stock stock = stockService.findBySku(item.getSkuId());
        if (stock == null || stock.getAvailable() < item.getQuantity()) {
            throw new IllegalArgumentException("库存不足:" + item.getSkuId());
        }
    }

    // 2. 计价
    BigDecimal total = BigDecimal.ZERO;
    if ("NORMAL".equals(req.getUserLevel())) {
        total = req.getTotalAmount();
    } else if ("VIP".equals(req.getUserLevel())) {
        total = req.getTotalAmount().multiply(new BigDecimal("0.9"));
    } else if ("SVIP".equals(req.getUserLevel())) {
        total = req.getTotalAmount().multiply(new BigDecimal("0.8"));
    } else if ("NEW_USER".equals(req.getUserLevel())) {
        total = req.getTotalAmount().multiply(new BigDecimal("0.85"));
    }
    // 优惠券
    Coupon coupon = couponService.findById(req.getCouponId());
    if (coupon != null) {
        if ("FIXED".equals(coupon.getType())) {
            total = total.subtract(coupon.getValue());
        } else if ("PERCENT".equals(coupon.getType())) {
            total = total.multiply(BigDecimal.ONE.subtract(coupon.getPercent()));
        }
    }
    if (total.compareTo(BigDecimal.ZERO) < 0) total = BigDecimal.ZERO;

    // 3. 创建订单
    Order order = new Order();
    order.setUserId(req.getUserId());
    order.setItems(req.getItems());
    order.setTotalAmount(total);
    order.setStatus("CREATED");
    order.setCreatedAt(LocalDateTime.now());
    orderService.save(order);

    // 4. 减库存
    for (Item item : req.getItems()) {
        stockService.deduct(item.getSkuId(), item.getQuantity());
    }

    // 5. 通知
    notificationService.sendSms(user.getPhone(), "订单已创建,编号:" + order.getId());
    notificationService.sendEmail(user.getEmail(), "订单已创建", "您的订单 " + order.getId() + " 已成功创建");

    // 6. 积分
    user.setPoints(user.getPoints() + total.intValue() / 10);
    userService.update(user);

    return order.getId();
}

这段代码的问题在哪?如果现在要加一个"跨境订单校验需要海关信息"、或者"支付成功后发微信通知"、或者"大促期间用阶梯计价"——你都得往这个方法里塞,最后它变成 500 行、800 行。坏味道清单:超长方法、散弹式修改(每加一个需求就要改它)、switch 蔓延(if-else 换用户等级或优惠券类型等于加分支)、重复代码(校验逻辑每个字段都写一遍 if 抛异常)。

四步重构:每步只改一个维度

重构不是一步到位,而是小步持续改善。每步都保持代码可编译、可运行。

第一步:提取方法——拆出边界

最粗的分解:把 1 校验、2 计价、3 创建订单、4 减库存、5 通知、6 积分分别提成独立方法。这一步不引入任何模式,纯靠"一段代码承担统一职责"的直觉。

java
public String createOrder(OrderRequest req) {
    validate(req);
    BigDecimal total = calculatePrice(req);
    Order order = buildOrder(req, total);
    deductStock(req);
    notifyUser(req, order);
    rewardPoints(req, total);
    return order.getId();
}

6 个方法各 20-50 行,主方法从 80 行缩到 8 行。改动风险极低(只移动代码,不改逻辑)。

第二步:责任链——校验可插拔

校验的条件越来越多(黑名单、库存、海关、风控...),全塞在 validate() 里又回到原样。用责任链把每个校验规则做成独立 Handler:

java
public interface OrderValidator {
    void validate(OrderRequest req);
    void setNext(OrderValidator next);
}

@Component
public class BlacklistValidator implements OrderValidator {
    @Override
    public void validate(OrderRequest req) {
        User user = userService.findById(req.getUserId());
        if (user.isBlacklisted()) throw new IllegalArgumentException("用户已被拉黑");
    }
}

@Component
public class StockValidator implements OrderValidator {
    @Override
    public void validate(OrderRequest req) {
        for (Item item : req.getItems()) {
            Stock stock = stockService.findBySku(item.getSkuId());
            if (stock.getAvailable() < item.getQuantity()) {
                throw new IllegalArgumentException("库存不足:" + item.getSkuId());
            }
        }
    }
}

@Order 注解控制链的顺序。新增一个"海关校验"只需要写一个 CustomsValidator implements OrderValidator,不需要改已有代码(OCP)。这条链用的是短路形式——任一校验失败就抛异常,不再继续。

第三步:策略——计价可替换

calculatePrice 里的 if ("NORMAL"...) / else if ("VIP"...) 是典型的 switch 蔓延。优惠券计价同理。把"用户等级折扣"和"优惠券"分别提取为策略:

java
public interface PricingStrategy {
    BigDecimal apply(BigDecimal original);
}

@Component
public class VipPricing implements PricingStrategy {
    @Override
    public BigDecimal apply(BigDecimal original) {
        return original.multiply(new BigDecimal("0.9"));
    }
}

// 注意:CouponPricing 需要手动构造并注册到 Map,因为 OrderRequest 是请求级别的对象
// 不能在 @Component 中自动注入
public class CouponPricing implements PricingStrategy {

    private final OrderRequest req;

    public CouponPricing(OrderRequest req) {
        this.req = req;
    }

    @Override
    public BigDecimal apply(BigDecimal original) {
        Coupon coupon = couponService.findById(req.getCouponId());
        if (coupon == null) return original;
        if ("FIXED".equals(coupon.getType())) {
            return original.subtract(coupon.getValue()).max(BigDecimal.ZERO);
        }
        // "PERCENT" 类型处理
        return original.multiply(BigDecimal.ONE.subtract(coupon.getPercent()));
    }
}

Map<String, PricingStrategy> 做路由,key 就是用户等级枚举值。新增一个"PLATINUM"等级只需要写一个新策略类并注册到 Map,计价方法里一个 if 都不加。

第四步:事件——通知解耦

通知和积分是"订单创建后"的副作用,和主流程没直接关系。用 Spring 事件把它们拆出去:

java
// 在 createOrder 方法末尾
eventPublisher.publishEvent(new OrderCreatedEvent(this, order, user, req));

// 监听器各自处理
@Component
public class SmsNotificationListener {
    @EventListener
    public void handle(OrderCreatedEvent event) {
        notificationService.sendSms(event.getUser().getPhone(), "订单已创建:");
    }
}

@Component
public class PointsRewardListener {
    @EventListener
    @Async
    public void handle(OrderCreatedEvent event) {
        int points = event.getOrder().getTotalAmount().intValue() / 10;
        userService.addPoints(event.getUser().getId(), points);
    }
}

通知和积分变成异步执行,主流程不再关心"创建完订单后还要做什么"。新增微信通知:写一个 WechatNotificationListener,一个 if 都不改。

重构后的完整流程

java
@Service
public class OrderService {

    @Autowired private List<OrderValidator> validators;
    @Autowired private Map<String, PricingStrategy> pricingStrategies;
    @Autowired private ApplicationEventPublisher eventPublisher;

    public String createOrder(OrderRequest req) {
        // 责任链:校验
        validators.forEach(v -> v.validate(req));
        // 策略链:计价
        PricingStrategy strategy = pricingStrategies.get(req.getUserLevel());
        BigDecimal total = strategy.apply(req.getTotalAmount());
        // 主流程
        Order order = buildOrder(req, total);
        orderService.save(order);
        deductStock(req);
        // 事件:通知 + 积分等副作用
        eventPublisher.publishEvent(new OrderCreatedEvent(this, order, req));
        return order.getId();
    }
}

与最初的 80 行 createOrder 相比,主方法从 80 行缩到 15 行,且每行开头的改动都不会影响其他部分:加校验规则只写新 Handler,加计价方式只写新策略类,加通知只写新监听器。

重构安全网

没测试就重构等于在雷区跑步。重构前,先把 createOrder 的输入输出用例写成单元测试,覆盖:

  • 正常下单(NORMAL 用户无优惠券)
  • VIP 用户折扣计算
  • coupon 满减计算
  • 库存不足抛异常
  • 黑名单用户抛异常

这些测试在重构过程中必须保持绿。每步重构后跑一次 mvn test,确保行为没变。

小步提交:建议每步一次 commit,message 写清楚改了啥("refactor: 提取校验为责任链模式")。这样如果某步出问题,git revert 只影响那一小步。

常见误区与小结

  • 还没写测试就动手:你以为只是提取方法,结果漏了一个边界条件,线上少扣了库存
  • 三行 if-else 也上模式:if-else 只有 2-3 个分支且不会增加,保持原样比上策略模式更清晰
  • 重构时改逻辑:重构是"不改行为只改结构",除非明确是 bug 否则不要顺手修(分开 commit)
  • 模式命名太抽象:类名叫 OrderHandler 不如叫 BlacklistValidator,模式是骨架,命名要暴露意图
  • YAGNI 过度:一行校验写一个接口加一个实现,纯属浪费。两次重复才抽象,三次才上模式

小结:设计模式的重构不是炫技,而是用组织代码的方式降低未来改动的成本。坏味道是信号,模式是方案。核心原则是 OCP——对扩展开放,对修改关闭。下一篇(本模块最后一篇)会回到全景视角,把所有 12 个主题串起来,给出一个完整的"设计模式知识地图"。

参考

参考:Martin Fowler《重构:改善既有代码的设计》;《Refactoring to Patterns》;《阿里巴巴 Java 开发手册》设计规约章节

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。
粤ICP备2026104257号-1