主题
用模式重构:从 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 开发手册》设计规约章节