策略模式实战:告别支付模块的if-else,代码结构全面优化

发布时间:2026/10/12 12:54:19
策略模式实战:告别支付模块的if-else,代码结构全面优化 上周帮一个小组做代码走查看到支付模块的Service里堆了七八层if-else每接入一种支付渠道就往里塞一段方法长得要三屏才能拉完。新渠道上线时负责人小心翼翼地插代码生怕碰坏前面的正常流程。我说这个问题的本质不是代码风格不够好而是结构选型出了偏差。行为型设计模式里的策略模式就是专门对付这种“算法成堆、选择逻辑混乱”的场景的。这篇文章是23种设计模式系列的倒数第二篇聊行为型里的策略模式。我不会上来就贴教科书定义而是按“问题 - 结构 - 实战改造 - 模式辨析 - 避坑经验”这条线走每一段都给能直接跑通的代码。不管你是正在补设计模式基础还是项目里已经遇到这个坏味道都能从中拿到可用的东西。1. 一段写满if-else的代码是怎么逼你上策略模式的1.1 先看一段典型的“坏味道”代码先来看一段很有代表性的代码。很多项目里真实存在表现形式可能略有不同但骨架基本一致public class PaymentService { public void pay(String channel, double amount) { if (balance.equals(channel)) { // 校验余额扣余额记录流水 System.out.println(余额支付 amount); } else if (bankCard.equals(channel)) { // 校验银行卡信息调用银行接口处理回调 System.out.println(银行卡支付 amount); } else if (scan.equals(channel)) { // 生成二维码等待扫码结果 System.out.println(扫码支付 amount); } else if (nfc.equals(channel)) { // 近场通信相关逻辑 System.out.println(NFC支付 amount); } else { throw new IllegalArgumentException(不支持的支付渠道: channel); } } }注释里的内容在真实项目里往往就是几十行甚至上百行的核心逻辑。我见过更夸张的版本每个分支里还有自己的try-catch、重试机制、埋点逻辑整个方法逼近300行滚动起来毫无头绪。这段代码的问题一目了然。第一每次新增渠道都要打开PaymentService修改万一改串了直接影响现有渠道。第二单元测试想覆盖所有分支需要构造一堆场景测试代码比业务代码还长。第三分支之间的公共逻辑——参数校验、日志埋点、事务控制——和渠道特有逻辑搅在一起想复用只能复制粘贴。第四如果条件再叠加用户等级、金额区间这些维度if-else直接变成“金字塔”。1.2 问题本质算法与“选择算法”的地方绑死了仔细看看这些分支你会发现每个分支都是一套完整且独立的“算法”。余额支付有余额支付的校验和扣减流程扫码支付有扫码支付的二维码生成和结果轮询流程它们之间几乎没有重叠。问题在于算法本身和“选择算法的判断逻辑”被硬塞到了同一个方法里。选择逻辑一多方法就膨胀算法一变化改动就被放大。今天加一个渠道明天换一个校验顺序后天又要调整异常处理所有风险都集中在一处。策略模式的处理思路很简单把每一套算法各自封装成一个类让这些类实现同一个接口外界在需要的时候选择其中一个再交给一个统一的上下文去执行。这样一来“选择算法”和“使用算法”的关注点被拆开了算法之间也彻底解耦互不干扰。GoF对策略模式的经典描述里有一句很关键的话“定义一族算法分别封装起来让它们可以相互替换让算法的变化独立于使用算法的客户端。”这里说的“客户端”在具体代码里往往就是Service或者Controller。1.3 什么场景才值得上策略模式先泼一盆冷水不是所有if-else都该死。判断标准其实很朴素我就按三个条件来筛查分支里的逻辑足够“重”不是一个赋值、一个返回就能结束的简单动作分支数量会持续增长今天三个下个月可能六个业务上确实存在运行时动态选择算法的需求如果这三个特征一个都不沾老老实实写if-else代码反而直白可读。模式是来驾驭变化的不是为了消灭分支而存在。这也是策略模式在实战中被误解最多的地方——很多人只记住了“把if-else换成策略类”却忽略了这套替换带来的类数量上升和跳转成本。2. 策略模式的三个角色一个最小骨架就长这样2.1 三个角色分别负责什么策略模式的结构可以概括成三个角色。把这三个角色理解透比记住任何一段完整代码都重要。第一个是策略接口Strategy定义算法长什么样。在Java里通常就是接口或抽象类里面声明的是一组公共方法比如“执行支付”。它不关心具体实现只定义契约。第二个是具体策略ConcreteStrategy实现接口的类每一类算法对应一个实现。回到支付场景余额支付是BalanceStrategy银行卡支付是BankCardStrategy扫码支付是ScanStrategy。每个类内部维护自己的完整算法。第三个是上下文Context持有当前策略的引用对外提供一个统一入口。客户端把选好的策略交给Context由Context去调用策略方法。Context不知道策略内部怎么实现只知道“这个策略能满足当前需求”。依赖方向很清晰客户端依赖ContextContext依赖策略接口具体策略实现策略接口。反过来具体策略完全不依赖Context和客户端。2.2 最小可运行示例写一个最干净的例子不带任何业务干扰帮助大家看清楚骨架本身。首先是策略接口public interface PayStrategy { void pay(double amount); }三个具体策略public class BalanceStrategy implements PayStrategy { Override public void pay(double amount) { System.out.println(使用余额支付 amount 元); } } public class BankCardStrategy implements PayStrategy { Override public void pay(double amount) { System.out.println(使用银行卡支付 amount 元); } } public class ScanStrategy implements PayStrategy { Override public void pay(double amount) { System.out.println(使用扫码支付 amount 元); } }然后是上下文public class PayContext { private PayStrategy strategy; public PayContext() {} public PayContext(PayStrategy strategy) { this.strategy strategy; } public void setStrategy(PayStrategy strategy) { this.strategy strategy; } public void executePay(double amount) { strategy.pay(amount); } }客户端这样使用public class Client { public static void main(String[] args) { PayContext context new PayContext(new BalanceStrategy()); context.executePay(100); // 运行时切换策略 context.setStrategy(new ScanStrategy()); context.executePay(50); } }运行后控制台依次输出“使用余额支付100.0元”和“使用扫码支付50.0元”。整个过程中PayContext的代码没有改过一行只是换了个策略对象行为就完全不同了。这就是策略模式最核心的威力。2.3 为什么需要一个Context而不是直接让客户端持有策略有朋友会问客户端直接new一个策略对象调用pay方法不就好了吗为什么还要套一层Context关键在于“统一收口”。在真实业务里策略不只是“调一下方法”那么简单。通常在调用策略之前要做参数校验、开启事务、埋点日志调用之后要处理结果、捕获异常、提交事务。这些公共环节如果散落在每个客户端就会复制粘贴成灾。Context就是承载这些公共逻辑的地方。客户端只需要“把合适的策略交给Context然后调一个入口方法”剩下的事情Context统一安排。用生活例子类比策略是司机Context是导航仪客户端是乘客。乘客告诉导航仪“我要打车还是坐地铁”导航仪负责叫车或者规划线路乘客不需要自己联系司机。换乘方式的时候乘客只改一个决定导航仪协调剩下的流程。2.4 这个骨架里的多态是怎么回事很多初学者盯着PayStrategy strategy new BalanceStrategy();这一行发呆不太明白到底发生了什么。其实这是Java多态的经典体现。变量声明类型是接口PayStrategy实际指向的是BalanceStrategy对象。调用strategy.pay(amount)时JVM会根据运行时的真实对象找到BalanceStrategy里的pay方法执行而不是看变量的声明类型。一句话编译看左边运行看右边。多态是策略模式的底层机制。正因为有了多态PayContext才能在不修改自身代码的前提下用同一个executePay方法去执行任何一个实现了PayStrategy接口的类。这也解释了为什么策略类必须实现同一个接口否则上下文无法统一调用。3. 实战改造把支付模块从“多重分支”变成“一行选择”3.1 从哪个分支开始动手回到第一节那个PaymentService。改造第一步不是写代码而是观察每个分支。观察点有两个分支的逻辑是否足够重分支之间的边界是否清晰比如余额支付分支里有“余额校验、冻结/扣减、流水记录”三件事边界清晰适合整个搬进BalanceStrategy里。扫码支付分支里有“生成二维码、等待回调、处理回调结果”同样是独立闭环。改造时直接按边界搬即可。有一件事要特别注意分支里那些公共逻辑比如日志记录、参数校验、事务开启千万不要复制到每个策略类里那是在制造新的重复。正确的做法是把公共逻辑留在Context或调用入口策略类只保存“这一个渠道特有”的逻辑。3.2 定义策略接口与改造后的Service为了后续扩展方便策略接口不用pay(double amount)这种裸参数而是用一个请求对象包一层public class PayRequest { private String orderNo; private String channel; private double amount; // 构造器、getter、setter 省略 }策略接口public interface PayStrategy { void process(PayRequest request); }这样一来将来新渠道需要额外参数比如银行卡要卡号扫码要回调地址时在PayRequest里加字段即可接口签名不用动。各渠道的策略类public class BalanceStrategy implements PayStrategy { Override public void process(PayRequest request) { // 1. 校验余额 // 2. 扣减余额 // 3. 记录资金流水 System.out.println(余额支付 request.getAmount()); } }BankCardStrategy、ScanStrategy结构类似不再重复贴代码。每个类只关注一件事把自己的渠道流程跑完整。改造后的支付Servicepublic class PaymentService { private final PayStrategyRegistry registry; public PaymentService(PayStrategyRegistry registry) { this.registry registry; } public void pay(PayRequest request) { PayStrategy strategy registry.get(request.getChannel()); strategy.process(request); } }到这里PaymentService的核心逻辑只有三行。所有渠道的具体逻辑都在各自的策略类里新增渠道不再碰PaymentService开闭原则顺理成章地满足了。3.3 策略注册表把“选择”也统一收口如果客户端每个调用方都要自己根据channel去判断该用哪个策略那只是把坏味道挪了个位置。所以需要一个集中管理策略的地方我这里用Map实现一个轻量注册表public class PayStrategyRegistry { private static final MapString, PayStrategy STRATEGIES new HashMap(); static { STRATEGIES.put(balance, new BalanceStrategy()); STRATEGIES.put(bankCard, new BankCardStrategy()); STRATEGIES.put(scan, new ScanStrategy()); } public static PayStrategy get(String channel) { PayStrategy strategy STRATEGIES.get(channel); if (strategy null) { throw new IllegalArgumentException(不支持的支付渠道: channel); } return strategy; } }这里有几个设计点值得展开。第一Map的key用渠道标识value用策略实例查表效率是O(1)比连环if-else清爽得多。第二策略实例在类加载时创建一次之后一直复用因为策略类本身是无状态的复用一个实例没有任何问题。第三找不到策略的时候直接抛出明确异常而不是返回null让调用方到处判空。调用侧变得非常干净PayStrategy strategy PayStrategyRegistry.get(request.getChannel()); strategy.process(request);3.4 在IoC容器里更优雅的做法注入策略Map如果项目用的是Spring这类IoC容器注册表可以更进一步连手动put都不用了直接让容器把同一接口的所有实现注入进一个MapComponent public class PaymentService { private final MapString, PayStrategy strategyMap; public PaymentService(MapString, PayStrategy strategyMap) { this.strategyMap strategyMap; } public void pay(PayRequest request) { PayStrategy strategy strategyMap.get(request.getChannel()); if (strategy null) { throw new IllegalArgumentException(不支持的支付渠道: request.getChannel()); } strategy.process(request); } }只要策略实现类注册为BeanIoC容器会把所有PayStrategy类型的Bean收集起来Map的key默认是Bean名称value是策略实例。以后新增渠道只需要新增一个实现类并注册Bean老代码一行都不用改。这就是依赖注入结合策略模式最典型的落地形态。用这种方式要留意一个细节如果某个地方单独注入了PayStrategy而容器里又存在多个策略Bean容器会因为候选不唯一报错。这时候需要在注入点用限定注解明确指出要哪一个或者干脆全部用Map注入避免歧义。3.5 改造前后的对比改造前一个方法几百行分支逐步叠加测试要覆盖所有分支组合才能安心发布改一处全链路回归。改造后一个方法三行渠道逻辑各归其位新增渠道加类不修改旧代码测试直接对着单一策略类写。维度改造前改造后新增渠道修改核心Service代码新增策略类并注册单元测试构造各种分支组合单独测试每个策略类公共逻辑与分支逻辑混杂在一起收口在调用入口统一处理代码定位在大方法里反复滚动查找按类名直接跳转变更影响面改动集中在核心服务改动局限在新增类内部4. 近亲颇多策略模式与状态模式、模板方法、工厂模式的边界4.1 最容易混淆的状态模式与策略模式行为型模式里策略模式最容易被拿出来和状态模式比较。两者结构确实像都是“一个上下文持有一个接口的引用”连类图都长得差不多但意图完全不同。状态模式的灵魂是“状态流转”。状态对象内部持有其他状态的引用对象状态的改变会引发行为的改变关键是状态之间的切换是自动的、由业务规则驱动的。比如订单从“待支付”流转到“已支付”再从“已支付”流转到“已发货”状态对象自己知道下一个状态是什么客户端不需要干预。策略模式的灵魂是“算法替换”。策略之间的切换由客户端主动决定策略之间互相不感知没有“下一个策略”的概念。这次用余额支付就用余额支付下次想用扫码就扫码完全看调用方的选择。用生活例子区分最直观状态模式像地铁运行列车从一个站开到下一个站下一站是明确的策略模式像你要去某个地方选择打车还是坐公交这是个自由决定随时可以后悔换一种方式。对比维度策略模式状态模式核心意图封装可互换的算法封装与状态相关的行为切换者客户端或上下文主动切换状态对象自身流转策略/状态之间关系互不知晓可能知道相邻状态典型场景折扣计算、支付渠道、消息发送订单状态机、审批流转4.2 模板方法同骨架换部件还是整体换算法模板方法模式也容易和策略模式扯在一起。模板方法的重点在于定义一个算法的骨架把某些步骤延迟到子类实现。它的重心是“骨架不变个别步骤可变”策略模式的重心是“算法整体一个接一个定义互相独立整体替换”。打个比方做蛋糕烘焙流程大体固定——配料、搅拌、烘烤、冷却变的是配料比例和烘烤温度这是模板方法而蛋糕、饼干、布丁是三种完全不同的甜品做法几乎不共享选哪个就用哪套完整流程这是策略模式。代码层面还有个显著区别模板方法靠继承实现子类复用父类骨架策略模式靠组合实现上下文持有策略对象引用。经典设计原则里一直强调“组合优于继承”所以当骨架稳定但步骤多变的时候优先考虑策略模式而不是模板方法的情况并不少见。4.3 工厂模式负责“造”策略模式负责“用”工厂模式和策略模式分工不同却经常合作。工厂是创建型模式关注“如何创建对象”策略是行为型模式关注“如何组织和使用算法”。上面实现的PayStrategyRegistry其实就是“简单工厂策略模式”的合作产物。它既承担了创建策略对象的工作也承担了选择算法的收口。客户端拿到的是一个策略接口引用根本不关心具体实现类是怎么new出来的。一句话总结这三对关系工厂解决“用哪个实现类的实例”策略解决“算法之间怎么组织、怎么替换”模板方法解决“骨架里哪些步骤由子类自定义”。摸清边界设计模式才不会用成一锅粥。5. 上手策略模式之后四个实战细节值得反复提醒5.1 策略对象要设计成无状态的最容易翻车的点在策略类里写可变状态字段。比如在BalanceStrategy里放一个“本次支付金额”的成员变量多线程环境下两个请求同时进来金额互相覆盖资金流水直接对不上。策略实现类在注册表或IoC容器里通常是单例的多个调用方共享同一个实例。所以策略类内部尽量不要有可变的成员变量所有业务数据都应该通过process(request)的参数传入用完即走。需要临时缓存的中间数据用局部变量承载或者用线程局部存储并确保在请求结束后清理。5.2 策略参数不一致时接口怎么设计一个很现实的难题不同渠道需要的参数差别很大。余额支付需要用户ID和金额银行卡支付还需要卡号、有效期、验证码扫码支付要订单ID和回调地址。如果把这些参数全写进PayRequest这个类会逐渐膨胀成“上帝对象”。我的做法是策略接口的参数统一用一个上下文请求对象里面只放通用字段渠道差异明显的字段在请求对象里放一个扩展字段对象或者Map。这样接口签名保持稳定策略类自己从请求对象里取它需要的部分。要避免的做法是为了“参数刚好匹配”给每个策略定义不同的接口方法那会让上下文彻底失去统一调用的可能。5.3 策略数量膨胀后的治理策略模式有一个副作用类的数量会随着算法数量线性增长。二三十个策略类的时候命名规范和注册方式就变得至关重要。我一般这样做策略类名遵守“动作后缀”的规范比如BalancePayStrategy、BankCardPayStrategy一眼看清职责用枚举或者配置文件记录渠道与策略的对应关系做配置化注册而不是散落在多个类里如果策略实在太多并且不少策略逻辑相近先考虑用模板方法模式收拢公共骨架再考虑组件化拆分。策略数量多到超过人的理解上限时问题就变成了模块划分问题而不是模式选择问题。先把策略按业务域拆成不同包、不同模块不要让所有策略类堆在一个文件夹里。5.4 什么时候该克制策略模式不是if-else的万能解药最后想吐槽一个现象为了“消除if-else”而使用策略模式会把简单问题复杂化。我见过有人把两个简单分支也拆成四五个类的代码读起来要先跳七八个文件反而比原来的if-else难维护得多。策略模式真正适合的信号是分支里是一整套较重的算法并且这些算法会持续演化。如果分支又少又轻直接写if-else代码反而更诚实。可以借用一个判断标准如果下次加需求时发现“又要加一个分支了”而且改动点遍布多个方法那策略模式值得考虑如果只是加一行判断老老实实写清楚就好。我在实际项目里的习惯是先把分支里的逻辑写出来观察它有没有形成独立闭环。独立闭环足够重才值得拆成策略类。这个习惯帮我避免了很多无意义的抽象。再多看几个例子回到自己代码里不要急着重构。策略模式的价值体现在“变化”二字只有当变化发生、且变化方向明确时它带来的灵活性才是正向收益。这篇从问题到实战基本把策略模式讲透了下一篇是行为型模式也是全系列的收尾到时候见。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询