ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

策略模式+Spring自动装配:重构支付回调if-else的完整实践

策略模式+Spring自动装配:重构支付回调if-else的完整实践 不知道你有没有接过那种看起来很简单一打开就想关掉的老接口。我最近重构一个支付回调服务里面那个分发方法一共 200 多行七八个if (channel.equals(...))分支挨个堆积每个分支里还各自牵扯对账、状态更新、消息推送。每次新对接一个渠道我都要在别人写了几年的代码里找到那个方法小心翼翼地把新分支塞进去生怕动错一个括号。改完之后代码评审时同事问了一句这方法怎么又变长了我一时语塞就是因为这句话我下决心把这块改造成策略模式 Spring 自动装配的方案。这篇文章就完整记录我从为什么要改、怎么设计到实际重构、踩坑收尾的全过程给同样被 if-else 支配的朋友一条可以直接照抄的路线。1. 被 if-else 支配的回调分发先看清痛点到底在哪1.1 一段典型的分支堆砌代码长什么样业务背景很朴素系统需要接收不同第三方支付渠道的回调有支付宝、微信、银联后续还要接京东、抖音支付。每个渠道的回调报文格式不一样、验签逻辑不一样、入账处理也不一样。最初版本只支持支付宝后来微信接入直接在原方法上追加else if再后来银联也接入继续追加。等我要接第四个渠道时代码已经长成这样public CallbackResult handleCallback(String channel, CallbackRequest request) { if (alipay.equals(channel)) { AlipayBiz alipayBiz new AlipayBiz(); if (!alipayBiz.verifySign(request.getParams())) { throw new BizException(支付宝验签失败); } String orderNo alipayBiz.getOrderNo(request.getParams()); Order order orderService.getByOrderNo(orderNo); // 更新订单状态、处理商品逻辑、发送站内通知... // 中间还有几十行业务代码 return CallbackResult.success(); } else if (wechat.equals(channel)) { WechatBiz wechatBiz new WechatBiz(); if (!wechatBiz.verifySign(request.getParams())) { throw new BizException(微信验签失败); } // 又是一大段几乎没有复用性的逻辑 return CallbackResult.success(); } else if (unionpay.equals(channel)) { // 第三个渠道逻辑继续复制粘贴 } else { throw new BizException(不支持的渠道); } }这段代码表面上是能用实际上四个问题一个比一个致命第一可读性极差。200 行方法从头到尾没有分节核心业务逻辑完全淹没在渠道判断里。新同事接手时根本分不清哪些代码是公共流程、哪些是渠道差异逻辑连定位一个 bug 都要来回滚动看半天。第二扩展成本高。每加一个渠道就要改动这个老方法而改一个每天都在线上跑的入口方法心理压力是巨大的。代码越加越长回归测试的范围也越来越大因为改动的位置太中心了谁都不敢保证动一个分支会不会影响另一个分支。第三可测试性几乎为零。想测试第四个渠道的逻辑得先跑完前三个分支的条件判断。想单独验证支付宝的验签逻辑抱歉你得构造整个handleCallback的调用链路连微信和银联的分支代码也会被加载进测试覆盖率里。第四很难复用。如果未来有其他接口比如主动查单、退款回调也要按渠道分发这段逻辑无法被复用只能再次复制一份然后继续膨胀。这种代码不是 烂代码 那么简单它是一个会让团队整体效率不断下降的坏味道。而且我要强调用 switch-case 替换 if-else 解决不了本质问题因为分支判断和业务逻辑仍然强耦合在一个方法里。哪怕写成 switch每加一个 case 仍然要动这个方法。1.2 为什么很多人知道策略模式却照样写不好有人说策略模式嘛谁不会。但我在代码评审里见过太多次伪策略模式——名义上建了策略接口实际干的事情并没有解决问题。最常见的三种变形变形一策略类内部继续 if-else。把所有渠道处理逻辑塞到一个大工厂类里工厂类的getHandler(channel)方法依然是 8 个条件判断。这只是把 if-else 从方法里挪到了工厂里换汤不换药。变形二手动维护策略注册表。知道要用 Map于是写一个static MapString, Handler然后在每个实现类的静态代码块里put(alipay, this)或者在一个集中配置类里手动 new 出来再 put。这样搞的问题在于策略实例的生命周期完全靠自己管理Spring 的依赖注入、AOP、代理这些能力全都浪费了而且很容易出现忘记注册导致运行时 NPE。变形三用反射或字符串拼接去做动态分发。比如根据 channel 拼出类名然后Class.forName反射调用。这种方案看起来巧妙实际上把类型安全彻底丢掉了类名一重构就全盘崩性能也受影响排查问题非常费劲。真正正确的姿势是什么就是利用 Spring 容器自己的自动装配能力把每个渠道的处理逻辑做成一个独立的 Spring Bean然后让 Spring 把同一接口下的所有实现类收集起来自动组装成一个 Map。调用方一行代码从 Map 里取出对应策略甚至不需要关心到底有哪些策略实现类存在。这个思路的核心转变是——选择权从调用方代码转移到了IoC 容器。2. 策略模式在 Spring 里的正确打开方式核心是让容器替你完成选择2.1 经典策略模式与 Spring 容器策略模式的差异先回忆一下经典策略模式的教科书写法有一个策略接口多个具体策略实现类然后一个 Context 类持有策略接口引用客户端在调用时手动选择具体策略传入 Context。核心思想是把算法独立封装使它们可以互相替换。到了 Spring 项目里这个模式有一个天然的升级版具体策略实现类交给 Spring 管理成为 Bean然后通过集合注入把容器内所有策略接口的实现类自动收集起来。调用方不再选择策略而是把选择条件交给 Spring 装配的结果——一个以渠道标识为 key 的 Map。很多人第一次看到这种写法时会觉得奇怪Map 也能注入这里要展开说说因为这是整个方案里的关键点也是很多人实际用错的环节。2.2 Map 注入与 List 注入Spring 集合注入能力的两种用法Spring 允许在注入点直接声明一个接口类型的集合比如Component public class PaymentDispatcher { private final MapString, CallbackHandler handlerMap; public PaymentDispatcher(MapString, CallbackHandler handlerMap) { this.handlerMap handlerMap; } }启动时 Spring 会扫描容器中所有CallbackHandler接口的实现类 Bean把它们按照beanName 作为 key、实例作为 value的方式组装成一个MapString, CallbackHandler注入进来。如果用 Listprivate final ListCallbackHandler handlers;那就得到一个包含所有实现类的 List顺序可以通过Order注解或实现Ordered接口来控制。这里有两个非常容易踩的细节第一个细节Map 的 key 默认是 Bean 的名字不是实现类上的任何自定义标识。如果你的实现类叫AlipayCallbackHandler那么默认 key 是alipayCallbackHandler首字母小写而不是alipay。如果你调用时handlerMap.get(alipay)拿到的就是 null。这就是很多人第一次改造失败的直接原因。第二个细节Map 注入不会触发类型转换或排序。Spring 会把所有匹配类型的 Bean 一次性收集进 Map如果你希望按特定的规则排列得自己在策略接口上设计优先级的概念或者使用Order配合 List 使用。聊清楚这两点之后你就可以看出来策略模式 Spring 自动装配的核心套路其实非常简洁定义策略接口接口方法就是业务处理的统一入口。每个分支逻辑写成一个实现类注册为 Spring Bean。用一个分发器Dispatcher / Holder注入策略 Map。调用方只依赖分发器传入业务类型标识拿到对应策略实例。扩展新渠道时只新增一个策略 Bean其他代码完全不动。这个套路比手动写工厂的优雅之处在于注册动作由容器完成天然避免了忘了 put 的问题。Spring 容器本身就是最大的工厂你只要把实现类定义成 Bean它就自动进了 Map。2.3 为什么推荐构造函数注入而不是字段注入在 Spring 策略集合注入的写法里我强烈建议用构造函数注入而且用final修饰字段。除了 Spring 官方推荐的不可变依赖原则之外在策略分发器这个特定场景里还有两个实际收益防止空指针。如果一个环节你忘了声明构造参数Spring 启动时直接报错而不是运行到某次调用时才发现 handlerMap 是 null。方便单元测试。构造函数注入意味着你可以直接new PaymentDispatcher(Map.of(alipay, mockHandler))来构造测试对象不需要启动 Spring 容器。我在项目里用 Lombok 的RequiredArgsConstructor配合final字段代码非常干净这也是目前 Spring Boot 项目里最主流的写法。3. 重构全过程从三大渠道 if-else 到策略分发器配完整代码3.1 先定义策略接口想清楚变化的是什么回到支付回调的例子。重构的第一步不是写实现类而是想清楚一个问题这段代码里什么东西是稳定的什么东西是变化的稳定的是整个回调处理的流程骨架——验签、取订单号、更新状态、返回结果。 变化的是每个渠道的验签方式、报文解析方式、可能还有一些不同的业务规则。策略接口就把变化的部分统一成一个方法签名。在这个场景里我定义接口如下public interface CallbackHandler { String getChannel(); CallbackResult process(PaymentCallbackRequest request); }这里我把getChannel()放进接口里有两个原因第一分发器需要一个渠道标识来决定 Map 的 key。虽然 Spring 默认会用 beanName 做 key但接口显式声明 getChannel() 可以保证策略实现类明确知道自己归属哪个渠道避免靠约定俗成。第二如果某个渠道的逻辑比较特殊未来需要一个 bean 处理多个 channel可以在这个设计上进一步扩展。当然也有人不喜欢在接口里放一个只返回常量的方法觉得这是代码味道。对于这种观点我比较务实在这个场景里它带来的明确性远大于理论上的抽象不纯而且它让新增策略类时开发者不得不把渠道 ID 写出来——这是一个强制约束反而能避免遗漏。3.2 实现三个策略类每个分支对应一个 Bean围绕接口开发支付宝、微信、银联三个实现类。这里以支付宝为例注意它是独立的 Spring Bean内部使用构造函数注入自己依赖的其他服务Component public class AlipayCallbackHandler implements CallbackHandler { private final OrderService orderService; private final AlipayBiz alipayBiz; public AlipayCallbackHandler(OrderService orderService, AlipayBiz alipayBiz) { this.orderService orderService; this.alipayBiz alipayBiz; } Override public String getChannel() { return alipay; } Override public CallbackResult process(PaymentCallbackRequest request) { if (!alipayBiz.verifySign(request.getParams())) { throw new BizException(支付宝验签失败); } String orderNo alipayBiz.getOrderNo(request.getParams()); Order order orderService.getByOrderNo(orderNo); // 订单状态更新、账务处理、通知发送... return CallbackResult.success(); } }微信、银联的实现类结构完全一致只是内部调用各自的 SDK 和业务方法。注意关键点每个实现类的类名、Bean 名是什么都不重要重要的是 getChannel() 返回的字符串。这就是后面从 Map 里取策略的 key。3.3 分发器从 Map 里取出对应策略分发器是整个模式里最薄、最核心的一层。它只做一件事根据 channel 从 Map 里找到对应的 handler 并调用。Component public class CallbackDispatcher { private final MapString, CallbackHandler handlerMap; public CallbackDispatcher(MapString, CallbackHandler handlerMap) { this.handlerMap handlerMap; } public CallbackResult dispatch(String channel, PaymentCallbackRequest request) { CallbackHandler handler handlerMap.get(channel); if (handler null) { throw new BizException(不支持的支付渠道: channel); } return handler.process(request); } }这里有个小细节值得说道说道handler 为 null 时不应该让空指针异常从业务代码里抛出来而是主动抛出带渠道名的业务异常。这是排查线上问题时最省时间的做法——你不会看到一条笼统的 NPE而是直接看到不支持的支付渠道: unknown_channel一眼定位是前端传错参数还是新渠道没注册。改造后原来的 Controller 调用从 200 行压缩成两行PostMapping(/callback/{channel}) public CallbackResult handleCallback(PathVariable String channel, RequestBody PaymentCallbackRequest request) { return dispatcher.dispatch(channel, request); }如果你担心getChannel()与 Map key 不一致导致取不到 handler也可以在Configuration配置类里手动注册一个基于getChannel()返回值的 Map。不过对于大多数场景我建议直接用 Spring 自动收集的 Map然后在getChannel()里返回和 key 一致的值就行因为这样的代码最精简。3.4 新旧代码对比这套方案的价值到底在哪重构完成之后把新旧代码摆在一起看差异是非常直观的维度改造前改造后新增一个渠道修改核心分发方法追加分支新增一个实现类加 Component修改某个渠道逻辑在长方法里找到对应分支直接打开对应策略类修改单元测试需覆盖整个分发方法单独测一个策略类即可回归风险改动可能在中心位置影响所有分支策略之间完全隔离理解成本200 行无分节一个薄分发器 多个瘦实现类这意味着重构之后我接第四个渠道的工作量从在别人代码里小心翼翼找位置变成了新建一个类写完业务逻辑重启完事。调用方代码零改动也不需要在配置中心或什么地方声明什么。这个体验差异真的只有踩过 if-else 泥潭的人才懂。4. 进阶玩法用自定义注解把渠道标识从方法里挪到注解上4.1 getChannel() 的局限当策略数量膨胀以后的维护压力策略接口里的getChannel()方法解决了渠道标识从哪来的问题但有一个小毛病每加一个策略都要重写一遍返回渠道名的样板代码。更麻烦的是如果未来一个实现类要负责多个渠道比如国内渠道和海外渠道都走同一套逻辑但标识不同getChannel()只能返回一个值就不够灵活了。这时可以进入进阶方案把渠道标识从方法签名里挪到注解上用元信息描述这个 Bean 是哪个渠道的策略。4.2 自定义一个 StrategyChannel 注解注解本身很简单就是一个带value()属性的标记注解Target(ElementType.TYPE) Retention(RetentionPolicy.RUNTIME) Component public interface StrategyChannel { String value(); }把Component也放进注解定义里这样实现类只需要标一个StrategyChannel(alipay)同时完成了注册为 Bean和声明渠道标识两件事少敲一个注解。使用方式StrategyChannel(alipay) public class AlipayCallbackHandler implements CallbackHandler { Override public CallbackResult process(PaymentCallbackRequest request) { // 直接是业务逻辑不需要 getChannel() 样板方法 } }4.3 通过 BeanPostProcessor 自动收集并注册有注解之后就需要一种机制把这些注解信息收集起来组成注册中心。在 Spring 里最合适的切入点是BeanPostProcessor——在每个 Bean 初始化完成后检查它是否带了StrategyChannel注解如果是就把它放进注册表。这里我定义一个轻量的注册中心Component public class StrategyRegistry { private final MapString, CallbackHandler registry new ConcurrentHashMap(); public void register(String channel, CallbackHandler handler) { registry.put(channel, handler); } public CallbackHandler get(String channel) { return registry.get(channel); } }然后写一个BeanPostProcessor在postProcessAfterInitialization阶段扫描注解Component public class StrategyChannelProcessor implements BeanPostProcessor { private final StrategyRegistry registry; public StrategyChannelProcessor(StrategyRegistry registry) { this.registry registry; } Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { Class? beanClass AopProxyUtils.ultimateTargetClass(bean); StrategyChannel annotation AnnotationUtils.findAnnotation(beanClass, StrategyChannel.class); if (annotation ! null bean instanceof CallbackHandler handler) { registry.register(annotation.value(), handler); } return bean; } }注意我在代码里故意用了AopProxyUtils.ultimateTargetClass(bean)而不是直接bean.getClass()。原因很实际如果这个策略 Bean 上挂了事务注解或者别的 AOP 切面Spring 注入进来的实际上是一个 JDK 动态代理对象bean.getClass()拿到的是$Proxy类findAnnotation会扫不到原始类上的注解。用AopProxyUtils.ultimateTargetClass拿到的是被代理的最终目标类注解信息才不会丢失。这个坑我见过好几个同事踩过这里提前帮你踩平。调用方从原来的handlerMap.get(channel)变成registry.get(channel)逻辑几乎不变。4.4 到底什么时候值得用注解方案不过在这里我要讲点实在话如果你的渠道策略只有三五个用前置的基础版 Map 注入就足够了没必要引入注解 BeanPostProcessor。理由有两条代码可读性成本是真实存在的。BeanPostProcessor 这种全局钩子比普通的 Map 注入难理解得多新同事看一眼为什么这个策略会自动注册进 registry需要先搞清楚 Spring 的生命周期机制。自动收集越隐式排错越难。出了问题时注解扫描的链路比显式getChannel()长得多一般人排查起来会更吃力。那注解方案应该用在什么时候我总结三个场景策略数量超过十个一个实现类需要绑定多个渠道标识或者团队里约定了一些标记性注解比如内部框架的组件注解。在这些情况下注解 注册中心带来的整洁度才明显超过它引入的复杂度。5. 实测中的坑与细节Map 注入、循环依赖、空策略这些拦路虎5.1 Map 的 key 到底是谁Bean 名覆盖还是自定义这是策略 Spring 自动装配方案里最高频的问题没有之一。前面已经说过Spring 自动注入的MapString, CallbackHandler默认 key 是 Bean 名。这意味着如果你的实现类叫AlipayCallbackHandler那么 key 是alipayCallbackHandler不是alipay。你的业务请求里传的 channel 是alipay如果直接用map.get(alipay)就会拿到 null然后一脸懵。解决方式有三种我按推荐程度排序方式一显式指定 Bean 名让 Bean 名和渠道名保持一致。Component(alipay) public class AlipayCallbackHandler implements CallbackHandler {这样 Map 的 key 自然就是alipay。但这种写法有一个隐患渠道标识被放到了类名之外的字符串里如果渠道标识改了Bean 名也要同步改否则失配。方式二继承ApplicationContext后手动处理。不推荐代码太长没必要。方式三不依赖默认的 Map 注入在Configuration配置类里手动构建一个以getChannel()为 key 的 MapBean public MapString, CallbackHandler callbackHandlerMap(ListCallbackHandler handlers) { return handlers.stream() .collect(Collectors.toMap(CallbackHandler::getChannel, Function.identity())); }这个方案我比较推荐把 List 注入进来然后靠策略类自己声明的getChannel()组装成 Map。这样 key 的语义是业务渠道标识而不是Spring Bean 名语义准确也不容易被重构影响。不过要留意Collectors.toMap在 key 重复时会抛异常如果两个不同实现类返回了同一个getChannel()应用启动直接报IllegalStateException: Duplicate key。这其实是好事把问题在启动阶段暴露出来而不是默默覆盖。如果真有同一渠道多策略按条件选择这种需求用toMap合并函数处理即可。5.2 策略 Bean 之间互相依赖引发的循环依赖问题我实际遇到过一次某个渠道的策略类里需要调用另一个策略类的公共方法比如公共的订单状态校验于是直接在策略 A 的构造函数里注入了策略 B。结果 Spring Boot 启动直接报错Description: The dependencies of some of the beans in the application context form a cycleSpring Boot 2.6 开始默认禁止循环依赖两个策略类互相依赖或者分发器与策略类互相依赖都会在启动时被检测出来。解决这个问题我建议从设计上打破循环策略类不应该依赖另一个具体策略类而应该把公共逻辑抽取到独立的 Service 组件里然后两个策略都去依赖那个 Service。如果确实改不了最后的兜底办法是配置spring: main: allow-circular-references: true这个开关我强烈不建议打开。它会让你陷入启动时没问题、运行时代理异常的更深的坑。循环依赖的本质是设计问题用策略模式本来就是为了解耦如果解耦之后反而出现互相依赖那一定是职责划分还不到位需要回头再看看接口方法设计。5.3 List 注入的顺序如何实现多策略按优先级尝试前面讲的是一渠道一策略的场景。但现实中还有一种更微妙的场景同一类业务有多个策略都声称自己可以处理只不过有优先级关系希望从最高优先级开始试直到有一个成功。这时策略接口就不能只暴露处理方法还需要暴露是否支持本次请求的判断。而注入方式要从 Map 改成 Listpublic interface DispatchHandler { boolean support(Order order); DispatchResult handle(Order order); }多个实现类天然存在优先级先后Spring 在注入 List 时是支持排序的只要实现类上标注Order(1) Component public class VipOrderHandler implements DispatchHandler { ... } Order(2) Component public class NormalOrderHandler implements DispatchHandler { ... }Spring 收集 List 时会根据Order的值从小到大排列然后分发器依次遍历、找到第一个support为 true 的处理器执行即可。这种写法在责任链和策略组的场景里非常好用但注意它和 Map 按渠道取策略 解决的问题不一样不要混用。5.4 空策略兜底别让 NPE 成为业务兜底逻辑策略 Map 的get方法天然存在返回 null 的可能。渠道标识拼写错误、请求参数大小写不一致、某个新渠道策略还没开发完成但是前端已经在调用——都会触发 null。我见过有的代码直接handlerMap.get(channel).process(request);一旦get返回 null 就是 NPE线上排查还不一定能立刻想到是渠道未注册。更好的是分发器里统一处理并抛出带上下文信息的业务异常public CallbackResult dispatch(String channel, CallbackRequest request) { CallbackHandler handler handlerMap.get(channel); if (handler null) { throw new BizException(No handler for channel channel , available handlerMap.keySet()); } return handler.process(request); }把handlerMap.keySet()拼进异常信息是我的个人习惯。线上一旦报错日志里直接把所有已注册的渠道也打出来了你一眼就能看出到底是没注册还是请求参数错误。这个小细节在排障时价值非常大。5.5 策略类的状态管理Bean 是单例别在成员变量里缓存业务数据Spring 默认的 Bean 作用域是单例也就是说所有请求共享同一个策略实例。这是一个很多人忽略的细节。如果你在策略实现类里写了Component public class AlipayCallbackHandler implements CallbackHandler { private String currentOrderNo; // 错误示例并发请求会互相覆盖 }那恭喜你上线第二天你就会收获一堆订单状态莫名错乱的工单。策略类必须是无状态的它可以通过注入依赖使用别人的服务但绝不能在自己的字段里缓存任何与具体请求相关的数据。所有请求上下文都应该通过方法参数传递。这是使用单例 Bean 的基本素养在策略模式里尤其要强调因为策略接口的方法往往参数比较少、处理流程复杂很考验自律。6. 策略模式的适用边界别为了消 if-else 而消 if-else6.1 哪些分支根本不需要改造讲完怎么做我必须讲讲什么时候不做。策略模式不是万能良药有些 if-else 是合理存在的不该为了优雅而强行优化。分支简单且固定比如性别判断、状态枚举映射这类只有两三个分支、几乎没有扩展可能、每个分支只有一两行代码的情况下用策略模式纯属过度设计。你建接口、写实现类、写分发器的工作量比 if-else 本身还大团队维护成本反而更高。分支条件不互斥一个请求可能同时触发多个逻辑分支比如既需要发短信又要发邮件这种情况下要的不是策略模式的多选一而是责任链或事件机制的多都执行。如果你硬套策略模式反而会把逻辑搞得更加拧巴。策略之间有共享的上下文状态策略模式希望策略是替换式的、无状态的但如果每个分支需要共享一个复杂的调用链状态比如长时间会话、分步操作那策略模式并不合适你应该考虑状态模式或临时存储。6.2 除了策略类还有哪些消灭 if-else的姿势在 Java 8 时代很多简单的分支处理可以用更轻量的手段枚举策略模式。如果一个业务的策略实现逻辑比较短且类型固定直接在枚举里定义行为是最紧凑的写法public enum PayChannel { ALIPAY { Override public void pay(Order order) { // 支付宝支付逻辑 } }, WECHAT { Override public void pay(Order order) { // 微信支付逻辑 } }; public abstract void pay(Order order); }这种方法在分支逻辑简单、不需要依赖外部服务、类型固定时非常干净。但如果策略实现需要注入其他 Service比如 orderService枚举里要想办法传入依赖设计上会稍微繁琐这时反而不如 Spring Bean 策略类顺手。Map Function。Java 8 之后接口不一定用传统的策略接口也可以直接用Function或者Consumer作为值MapString, FunctionOrder, Result actionMap new HashMap(); actionMap.put(alipay, order - payByAlipay(order)); actionMap.put(wechat, order - payByWechat(order));适合轻量、单方法的场景。缺点是可扩展性弱多个方法时 Function 表达不了业务逻辑复杂了还得回到显式接口。规则引擎。当分支条件不再是简单的channel.equals(...)而是多维度组合、几十上百条规则时策略模式也会力不从心。规则引擎Drools、Easy Rules 之类可以把条件和动作解耦得更彻底但引入成本高一般项目到不了这个量级不要轻易用。6.3 我的选型原则决策树综合多年实践我在决定要不要用策略模式时脑子里其实是一个简单的决策树分支数量超过 4 个且未来有明确的增长预期——是考虑策略模式。分支逻辑是否超过 10 行——是值得拆如果每个分支只有一两行先忍住。分支是否是互斥选择而非都要执行——是适用策略模式不是考虑责任链。各分支之间是否共享复杂的上下文状态——是慎用策略模式。团队内是否都熟悉 Spring 集合注入——如果只有你懂你写的代码再好三个月后没人敢动也是隐患。这套决策逻辑帮我避免了很多次为了炫技而踩坑的冲动。技术选型永远不是哪种模式最优雅而是哪种模式在团队的长期维护成本里最划算。写到最后的一点点个人经验这次重构做完之后我最大的感受是策略模式 Spring 自动装配这套组合真正的价值不是消除了 if-else 这个表面结果而是改变了代码的增长方式。以前每加一个渠道是在给一个已经有 200 行的老房子加一层地板现在每加一个渠道是给一棵树添一根独立的树枝。新增代码不需要担心碰坏旧逻辑测试时也只需要盯着新策略类本身。如果你准备动手重构类似的代码我的建议是先别急着写代码花十分钟把现有 if-else 的每个分支列出来找出稳定的流程骨架和变化点把接口方法设计想清楚再动手。接口设计对了后面所有实现类都是水到渠成的事接口设计错了后面会有更多的 if-else 等着你。祝你好运希望下次代码评审时你也敢自信地说这个方法已经不需要再改了。
返回列表