重构支付模块时,我把 600 行 if-else 拆成了四个模式
上个月接手支付模块重构。打开 PaymentService.pay() 那一刻我有点懵:一个方法 600 多行,开头十几个 if (channel == WECHAT) ... else if (channel == ALIPAY) ...,每个分支里又套着退款、对账、回调验签的逻辑。加一个新渠道(数字人民币)要改七八个地方,上次同事改漏了一处,导致对账单差了三天。
我借这次重构,把几个设计模式真正落到业务里。这里记的不是"模式是什么",是"在订单和支付里怎么用才不别扭"。
策略模式:把渠道的差异收进各自的类
支付渠道的差异点其实很清晰:下单、退款、验签、查单,四个动作,每个渠道实现不同。先抽一个接口:
public interface PayChannel {
PayType type();
PayOrderResult prepay(PayRequest req);
RefundResult refund(RefundRequest req);
boolean verifyCallback(String raw, String sign);
QueryResult query(String orderId);
}
微信、支付宝、银联各写一个实现类,注册到 Spring 容器,用一个 Map<PayType, PayChannel> 做路由:
@Component
public class PayChannelRouter {
private final Map<PayType, PayChannel> channels;
public PayChannelRouter(List<PayChannel> list) {
this.channels = list.stream()
.collect(Collectors.toMap(PayChannel::type, c -> c));
}
public PayChannel route(PayType type) {
PayChannel c = channels.get(type);
if (c == null) throw new UnsupportedChannelException(type);
return c;
}
}
加数字人民币时,只需要新增一个 DigitalRmBPayChannel 实现类,老的七个地方一个都不用动。这是策略模式最大的甜头:开闭原则真的成立。
模板方法:把"流程骨架"和"步骤差异"分开
退款链路有个固定流程:校验订单 → 调渠道退款 → 更新本地状态 → 发消息通知。前面校验和后面通知对所有渠道都一样,只有"调渠道退款"不同。
这正好是模板方法的位置。把不变的放父类,变化的留 abstract:
public abstract class AbstractRefundTemplate {
public final RefundResult refund(RefundRequest req) {
validate(req); // 公共:校验
RefundResult r = doRefund(req); // 抽象:渠道差异
updateLocalStatus(req, r); // 公共:落库
notify(req, r); // 公共:通知
return r;
}
protected abstract RefundResult doRefund(RefundRequest req);
}
final 很关键,它锁死了流程顺序,子类改不了。之前那个 bug——同事漏改了一处对账逻辑——正是因为逻辑散落在外,模板方法把它收进骨架后,不可能再漏。
责任链:把"校验"从主流程里摘出来
支付前置有一堆校验:金额是否合法、账户是否冻结、是否触发风控、渠道是否可用。原来它们全堆在 pay() 开头,顺序混乱。我用责任链把它们串成管道:
public interface PayFilter {
void doFilter(PayContext ctx, FilterChain chain) throws PayException;
}
// 每个校验一个类:AmountFilter / FrozenFilter / RiskFilter / ChannelFilter
@Bean
public PayFilterChain buildChain(List<PayFilter> filters) {
return new PayFilterChain(filters); // 按 @Order 排序
}
好处是顺序可配、可插拔。风控规则要加一条?新增一个 Filter 类加个 @Order 就行,主流程零改动。压测时我还发现 RiskFilter 平均耗时 23 ms,把它排到金额校验(0.2 ms)后面,能在更早的阶段拦掉非法请求,省掉不必要的风控调用。
观察者:状态变了,通知各干各的
订单状态变更后要联动很多事:发 App 推送、记流水、给积分、触发履约。这些"事后动作"不该Blocking在主事务里。我用 Spring 的事件机制(本质就是观察者)解耦:
@EventListener
@Async
public void onPaid(OrderPaidEvent e) {
pushService.notify(e.getUserId(), "支付成功");
pointService.add(e.getUserId(), e.getAmount());
fulfillmentService.trigger(e.getOrderId());
}
@Async 让通知异步跑,主流程只发事件。后来运营要加"支付后发优惠券",加一个 @EventListener 方法即可,订单核心逻辑完全不知道优惠券的存在。
踩到的坑
- 策略模式别过度。我们一开始把"查单"也塞进策略接口,结果三个渠道查单逻辑一模一样,反而制造了重复。后来把公共部分下沉到抽象基类,只有真有差异的方法才留在接口。
- 责任链里别做重 IO。有次把风控放链头同步调,一次支付 P99 从 80 ms 涨到 210 ms。重校验要么异步、要么前置到链尾的轻校验之后。
- 观察者用
@Async要注意异常被吞。某次积分服务挂了,异常在异步线程里丢了,订单照样成功但用户没拿到积分。后来给@Async配了AsyncUncaughtExceptionHandler才看得见。
下篇预告
这篇先把《设计模式在业务系统中的真实应用》里的坑列了,下一篇写我们当时是怎么在线上工程里真正落地的——包括那次让领导拍桌的故障复盘。