Administrator
发布于 2022-02-28 / 3390 阅读
76

设计模式在业务系统中的真实应用

重构支付模块时,我把 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 才看得见。

下篇预告

这篇先把《设计模式在业务系统中的真实应用》里的坑列了,下一篇写我们当时是怎么在线上工程里真正落地的——包括那次让领导拍桌的故障复盘。

参考