☰
策略模式实战:Java支付场景如何优雅干掉if-else
2026/10/13 22:45:43 网站建设 项目流程

1. 策略模式到底是啥:先丢掉设计模式教科书

Java项目里一旦业务分支多起来,最先爆炸的往往不是数据库,而是那个又长又臭的if-else方法。举个最常见的例子:支付。今天要接支付宝,明天要接微信,后天老板说要不上个银行卡支付吧。第一版代码简单,if (channel.equals("alipay"))一路写下去,第二版也能忍,第三版发现每个渠道还需要不同的参数校验、不同的回调签名、不同的日志埋点,一个方法从十几行膨胀到几百行,谁接手谁想哭。

策略模式就是来收拾这种局面的。它做的事说白了就一句话:把每一个"如果"变成"一个独立的类",让调用方只认一个统一的入口,至于内部到底是支付宝还是微信还是银行卡,由对象自己在运行期决定。

很多教程喜欢从定义讲起,什么"定义一族算法族,分别封装起来,让它们之间可以互相替换,算法的变化独立于使用算法的客户"。这个定义本身没错,但太干,初学者看了三遍还是不知道代码怎么落。我换个方式讲。

1.1 一个生活化的例子

把策略模式想象成买咖啡。你站在吧台前,核心需求是"我要一杯咖啡",但杯型有小杯、中杯、大杯,去冰、少冰、常温,加糖、不加糖。如果店员把所有组合都写在一张纸上,每次你点单它都要从上往下扫一遍"小杯去冰加糖?不是。中杯去冰加糖?也不是……",那就是if-else配合全家桶的方式。

策略模式的做法是:把"做哪一杯"这件事拆成两种维度,一个是"怎么做"(策略),一个是"今天给我做哪一杯"(选择权)。你下了单,店员拿出对应那张配方卡(策略对象),照着做。下次你换杯型,店员换张卡就行,不需要把整本菜单重抄一遍。

放在代码里,"配方卡"就是具体策略类,"店员"就是上下文类,"点单"就是调用入口。策略模式的核心思想浓缩成三个角色:策略接口,负责定义"怎么做"的能力;具体策略,负责实现每一种做法;上下文,负责持有策略并在合适的时机调用。

1.2 策略模式要解决的痛点是"代码分叉"

写业务代码时,最需要防范的不是算法太难,而是分支太多。每多一个else,方法圈复杂度就上涨一次,测试用例数量也会跟着涨,改一个分支的时候还容易波及另一个分支。这种问题在代码里有个专门的称呼:代码分叉。分叉越多,代码越难测试、越难扩展、越难跟同事解释。

策略模式的切入点就在这儿。它不是把你代码里的分叉消灭掉,而是把分叉从"调用方的代码里"挪到"一组平行策略类里"。用接口把变化收敛起来,调用方永远只跟接口谈,后面加新策略只是加一个类,改老策略只是改一个类,谁也不会影响谁。

1.3 模式骨架长什么样

策略模式的标准类图很简单,代码结构其实也就三样东西:

策略接口 ├── 具体策略A ├── 具体策略B └── 具体策略C 上下文类(Context) ├── 持有策略接口 └── 对外暴露执行入口

上下文类可以简单理解成"一个拿着策略干活的人"。它不关心策略内部怎么做,只关心自己手里能不能拿到一个策略接口。这个持有方式可以是构造器传入、Setter 注入,也可以是一个策略注册表,后面会讲到实用的几种写法。

新手容易把策略模式和"多态"混在一起理解。策略模式确实依托多态,但它强调的重点是"行为选择权的转移":调用方把"选哪个行为"的决策从一堆 if 里抽离出来,交给一套独立的机制去处理。多态是语言特性,策略模式是设计思想,两者是基础和上层建筑的关系。

2. 零基础手写第一个策略模式

光讲理念没用,代码不落地等于白讲。这一段我从一个支付场景出发,走一遍最朴素的策略模式实现路径。这个案例不引入 Spring、不引入任何框架,纯 Java 代码,重点是把四个角色写明白。

2.1 第一步:定义策略接口

接口是用来约束"每个策略都必须能干这件事"的。支付场景里,每一条策略至少要做两件事:判断自己能不能处理当前这个支付渠道;执行实际的支付动作。接口可以定义成下面这样:

public interface PayStrategy { /** * 判断当前策略是否支持该渠道 */ boolean supports(String channel); /** * 真正执行支付 * * @param amount 金额 * @param orderId 订单号 */ void pay(BigDecimal amount, String orderId); }

support这一步很多人会忽略,但它在生产环境里非常关键。因为策略模式在运行期要解决的问题之一就是"我有好几个策略,到底该用哪一个",support就是策略实例自己给出的答案。这比调用方逐个判断要清爽得多。

提示:接口方法的命名尽量保持业务可读性。项目里见过有人把supports写成isSupport的,也有人把pay写成handleBusiness的,不是说不能这么写,但命名越贴近业务,后面看代码的人越省力。

2.2 第二步:实现具体策略类

接口定义完之后,每一种支付方式写一个实现类。以支付宝策略为例:

public class AlipayStrategy implements PayStrategy { @Override public boolean supports(String channel) { return "ALIPAY".equals(channel); } @Override public void pay(BigDecimal amount, String orderId) { // 这里的逻辑可以是真实的调用支付宝接口 System.out.printf("支付宝支付[订单号:%s, 金额:%s]%n", orderId, amount); } }

微信策略、银行卡策略同理,都是实现同一个接口,把各自渠道的差异封装在自己的类里面。这时候你会发现一个巨大的好处:每个策略类只关心自己这一亩三分地,不需要知道别家策略的存在。

需要注意的是,具体策略类不一定非得是个"纯业务类"。它的内部可以组合其他工具类、Mapper、第三方客户端,这些都无所谓,只要对外仍然实现PayStrategy就好。这也是策略模式能承载复杂业务的关键所在——它并不阻止策略类内部变复杂,它只是保证"复杂不出圈"。

2.3 第三步:写一个上下文类做"策略调度"

上下文类在策略模式里承担的是"中间人"的角色。它提供一个统一的入口,内部去遍历手里的策略列表,找出能干活的那一个,然后让它干活。最简单的上下文实现长这样:

public class PayContext { private final List<PayStrategy> strategies; public PayContext(List<PayStrategy> strategies) { this.strategies = strategies; } public void execute(String channel, BigDecimal amount, String orderId) { PayStrategy strategy = strategies.stream() .filter(s -> s.supports(channel)) .findFirst() .orElseThrow(() -> new IllegalArgumentException("不支持的支付渠道: " + channel)); strategy.pay(amount, orderId); } }

这段代码的核心就一行流式操作:从策略列表里过滤出支持当前渠道的策略,找不到就抛异常。这种"遍历策略列表 + 按条件匹配"的方式在生产项目里非常常见,因为它的扩展方式很优雅:新增一个渠道,往strategies列表里加一个策略对象,PayContext一行都不用改。

如果不想用集合加遍历,也可以用 Map 把渠道编码和策略映射起来:

public class PayContext { private final Map<String, PayStrategy> strategyMap; public PayContext(Map<String, PayStrategy> strategyMap) { this.strategyMap = strategyMap; } public void execute(String channel, BigDecimal amount, String orderId) { PayStrategy strategy = strategyMap.get(channel); if (strategy == null) { throw new IllegalArgumentException("不支持的支付渠道: " + channel); } strategy.pay(amount, orderId); } }

两种写法都能跑。第一种适合策略数量不多、匹配条件灵活的场合;第二种适合渠道编码稳定、希望 O(1) 级别时间复杂度的场合。实际生产里我见过最多的还是第一种,因为项目里策略匹配条件经常不只是字符串相等,可能还要判断订单金额、用户等级、库存状态等等,supports方法能表达更复杂的逻辑。

2.4 第四步:在项目中调用并测试

写完上下文和策略类,调用就非常简单了:

public class Application { public static void main(String[] args) { List<PayStrategy> strategies = List.of( new AlipayStrategy(), new WechatPayStrategy(), new BankCardStrategy() ); PayContext context = new PayContext(strategies); context.execute("ALIPAY", new BigDecimal("99.90"), "20240001"); context.execute("WECHAT", new BigDecimal("19.90"), "20240002"); context.execute("BBCARD", new BigDecimal("299.00"), "20240003"); } }

输出结果依次是支付宝、微信、银行卡三条支付日志。整个过程调用方不接触任何策略实现类,新增支付渠道时只需要新增一个实现类并加到列表里,原有关键代码零改动。这就是策略模式中最典型、最直观的闭环。

3. 进阶实战:策略模式在生产项目里的正确姿势

基础版本能跑后,真正的挑战才开始。生产项目里策略模式要面对的不只是"跑起来",而是:策略实例怎么管理?策略和 Spring 容器怎么融合?策略多了以后维护性怎么保证?这一节讲的都是我在项目里实际用下来觉得靠谱的做法。

3.1 策略注册表:怎么优雅地拿到想要的策略

上面演示的PayContext里,策略列表是手动传给构造器的。这在测试、工具类、小项目里没有毛病,但生产项目通常使用 Spring 管理 Bean,手动new策略类会让 Spring 的依赖注入优势荡然无存。主流的方案是做一个"策略注册表",把所有策略按名字集中注册,需要的时候从注册表里取。

策略注册表本质就是个 Map 结构,key 是策略标识,value 是策略实例。既然要用 Spring 管理,最省事的做法是利用依赖注入的特性,把 Spring 容器里所有PayStrategy类型的 Bean 直接收集成一个 List,再转成 Map:

@Component public class PayStrategyRegistry { private final Map<String, PayStrategy> strategyMap = new HashMap<>(); public PayStrategyRegistry(List<PayStrategy> strategyList) { for (PayStrategy strategy : strategyList) { strategyMap.put(strategy.getChannel(), strategy); } } public PayStrategy getStrategy(String channel) { PayStrategy strategy = strategyMap.get(channel); if (strategy == null) { throw new IllegalArgumentException("找不到支付策略: " + channel); } return strategy; } }

对应的策略接口需要增加一个getChannel()方法:

public interface PayStrategy { String getChannel(); void pay(BigDecimal amount, String orderId); }

这样写的好处有三个。第一,新增策略只需要新增一个 Spring Bean,注册表自动收集,不需要像手写代码那样手动往列表里添加。第二,通过getChannel()显式声明身份,比supports(channel)这种逐个匹配的方式性能更好。第三,注册表的对外接口非常干净,业务方只依赖注册表,不依赖具体策略类。

3.2 结合Spring:策略注入与工厂模式的搭配方式

策略注册表解决了"策略如何获取"的问题,但它和工厂模式到底有什么区别?这是很多人问过我的问题。简单说:策略模式关心的是"算法的封装和替换",工厂模式关心的是"对象的创建和获取"。策略注册表在获取策略时用到了一点工厂思路,所以实战里两者经常混着用,这很正常,宋体字教科书上去区分是学术洁癖,业务代码里好用就行。

在 Spring 项目里,还有一种常见的玩法是把策略选择和上下文切分开。上下文类只管调用策略,策略的获取交给注册表:

@Component public class PayService { private final PayStrategyRegistry registry; public PayService(PayStrategyRegistry registry) { this.registry = registry; } public void pay(String channel, BigDecimal amount, String orderId) { PayStrategy strategy = registry.getStrategy(channel); strategy.pay(amount, orderId); } }

到这里已经能看出一个很清晰的依赖链条:Controller / Service -> PayService -> PayStrategyRegistry -> PayStrategy 实现类。每一层都只依赖自己前面那一层的接口,替换底层实现时上层完全无感。这种结构的可测试性也高——想测试PayService的时候,只要 mock 掉注册表就行,不需要真实的各种支付渠道。

3.3 用lambda表达式简化简单策略

我记得有段时间,团队成员为了体现自己用了策略模式,每个策略都单独写一个类,一个接口下有十几个实现类,类文件数量看着吓人。其实如果一个策略的实现只是简单地算个价格、做个小转换,完全没必要一个策略一个类。Java 8 的 Lambda 表达式和设备方法引用在这种场景下特别省事。

平时写电影的计费规则,假设有三档计价策略:普通用户原价、会员八折、VIP六折。用枚举加 Lambda 写,代码可以非常紧凑:

public enum MoviePriceStrategy { NORMAL("普通用户", amount -> amount), MEMBER("会员", amount -> amount.multiply(new BigDecimal("0.8"))), VIP("VIP", amount -> amount.multiply(new BigDecimal("0.6"))); private final String description; private final Function<BigDecimal, BigDecimal> calculator; MoviePriceStrategy(String description, Function<BigDecimal, BigDecimal> calculator) { this.description = description; this.calculator = calculator; } public BigDecimal calculate(BigDecimal original) { return calculator.apply(original); } public String getDescription() { return description; } }

使用的时候一行搞定:

BigDecimal price = MoviePriceStrategy.VIP.calculate(new BigDecimal("100.00"));

这段代码用 Lambda 把策略实现"内联"进了枚举,既避免了类爆炸,又保留了策略模式的核心精神:行为可替换。如果你手上的策略没复杂到要抽独立类的地步,Lambda 加枚举是比硬写一堆类更优雅的选择。

3.4 枚举策略:另一种易维护的写法

说到枚举,再多说两句。策略模式有一种"浓缩版"形态,就是把"策略接口 + 策略实现"压进一个枚举里面。枚举里的每个常量相当于一个策略实例,可以同时保留状态和行为。这种写法的好处是结构紧凑、一看就懂,坏处是开闭原则的彻底性差一点——要加新策略,必须改枚举本身。

取舍标准我总结了一下:如果策略类型相对稳定,数量不多(比如电商平台的订单状态:待支付、已支付、已发货、已完成),用枚举策略完全没问题;如果策略类型经常变、需要不断扩展(比如游戏里的宠物技能、优惠促销玩法),那就规规矩矩写接口加实现类。场景不同选择不同,没有绝对的正确。

3.5 实战案例:支付渠道选择器的完整实现

把上面的思路汇总一下,我看写一个"完整版"支付渠道选择器,方便你直接拿去改一改就能用。项目背景是某电商平台,需要支持支付宝、微信、银行咔三种支付方式,每种的参数校验、支付动作和回调处理都不一样。

先定义策略接口:

public interface PayStrategy { String getChannel(); void validate(PayRequest request); void pay(PayRequest request); void handleCallback(String callbackData); }

再写一个抽象模板类,把公共逻辑沉到父类去:

public abstract class AbstractPayStrategy implements PayStrategy { @Override public void pay(PayRequest request) { validate(request); doPay(request); recordLog(request); } protected abstract void doPay(PayRequest request); private void recordLog(PayRequest request) { // 统一的日志记录 } }

具体策略比如支付宝:

@Component public class AlipayStrategy extends AbstractPayStrategy { @Override public String getChannel() { return "ALIPAY"; } @Override public void validate(PayRequest request) { // 校验支付宝特有参数 } @Override protected void doPay(PayRequest request) { // 调用支付宝SDK发起支付 } @Override public void handleCallback(String callbackData) { // 处理支付宝回调,验签、更新订单状态 } }

这里加了一个抽象父类AbstractPayStrategy,目的就是让所有策略共用"校验、支付、记日志"的模板流程,同时把差异留在子类里实现。这种"模板方法 + 策略模式"的组合拳非常常见,也特别适合支付这类业务流程固定的场景。每个具体策略只需要关注"我这次支付要校验什么、要调用什么接口",公共流程由父类把控,代码复用率明显提高。

4. 从源码与语言机制角度看策略模式的应用

策略模式能成为生产环境常见的模式,不只是因为它"写起来方便",更因为它能真正改变代码的组织方式。这一节我挑几个实际案例和源码细节来讲,看完你会有一种"哦,原来人家早就这么用了"的感觉。

4.1 JDK里藏着的策略模式案例

很多人没意识到,JDK 本身就是策略模式的重度用户。最典型的是java.util.Comparator接口。Collections.sort()的排序规则可以通过传入不同的Comparator实现来改变,排序算法和排序规则是完全分离的,这就是策略模式——排序算法是固定框架,Comparator是策略,传入升序就是个升序策略,传入按年龄排序就是个年龄策略。

另一个经典案例是ThreadPoolExecutor的拒绝策略。它有一个RejectedExecutionHandler接口,JDK 默认提供了四种实现:AbortPolicy(直接抛异常)、CallerRunsPolicy(调用者线程执行)、DiscardPolicy(静默丢弃)、DiscardOldestPolicy(丢弃最老的未执行任务)。你在构造线程池的时候传入不同的拒绝策略,线程池的"穿不下时的处理方式"就随之改变,线程池主流程代码不用动分毫。这就是教科书级别地把策略模式嵌在框架里。

这两个例子有个共性:框架作者早就知道,分支逻辑不能写死在核心流程里,必须交给调用方去指定。该用谁、怎么用,是策略的事,框架只管"拿到策略,按策略干活"。

再往深说一层,Java 本身在语法层面也给你注入了策略模式的辅助工具:匿名内部类、Lambda、方法引用。在 Java 8 以前,写一个策略要写一个类,很重;Java 8 以后,函数式接口可以strategy -> ...直接表达,很多简单策略根本不需要类文件。Comparator.comparing(Person::getAge)这行代码本身就是"一个策略的创建",只不过它的表达成本从"新建一个类"降到了"一次方法引用"。这是策略模式最容易落地的一面,也是新老代码风格的重要分水岭。

4.2 策略模式与状态模式、工厂模式的边界

聊到模式,总绕不开边界问题。策略模式、状态模式、工厂模式三者经常被人混淆,我简单说下我自己在项目里是怎么区分它们的。

状态模式关注的是"对象内部状态改变导致行为改变",状态之间是有转换关系的,而且通常是对同一个对象情境下的状态流转,比如订单状态机:待支付 -> 已支付 -> 已发货。状态模式里的状态对象自己知道下一个状态是什么。策略模式关注的是"算法的替换",策略之间是平行的、互不依赖的,客户主动决定选哪个策略,策略不会自己切换到另一个策略。

工厂模式关注的是"对象的创建",它解决的是"你告诉我需求,我给你造出合适的对象",创建完之后对象干什么,工厂不操心。策略模式解决的是"对象已经拿到了,怎么用一个统一流程去执行它"。所以项目里经常是"工厂创建策略 + 策略执行动作",两者一前一后搭配合作。

我在支付中心重构的时候跟同事说过一句话:你先问自己三个问题——这个逻辑会有多个平行实现吗?这些实现会互相切换吗?调用的主流程想不想暴露给业务方?三个问题都答"是",放心用策略模式;如果只是犹豫"我该 new 哪个类",去用工厂模式;如果在互相关心对方的存在,那可能是状态机。

5. 常见问题与排查技巧实录

策略模式用了几年,多多少少踩过一些坑。下面这些问题大部分是我自己或身边朋友在真实项目里碰到的,整理出来给想上手的同学提前做个心理建设。

5.1 策略类爆炸问题:策略太多,地图变瀑布

策略一多,工程目录下密密麻麻全是策略类,找一个文件可能要滚半天。有人会说"这是策略模式的原罪",但其实不是。我见过一个项目里策略有三十多个,每个策略类里真正干活的就是一个方法,这种场景完全可以用枚举 + Lambda 或者策略分组来收敛。

策略分组是我比较推荐的做法。比如把策略按业务域拆分:支付策略、营销策略、消息发送策略分别放到不同的包下,接口也拆开。再比如把一些相近的策略合并成"带参数的策略",一个策略类内部通过参数决定细微差异,而不是每个分支都开一个类。把"策略的数量"控制在人类认知范围内是架构设计的一部分,不能只看模式不看治理。

5.2 空指针与策略未找到:如何在注册表入口兜底

策略注册表用 Map 存策略时,最容易出现的问题就是 key 拼写不一致,比如策略里getChannel()返回"WECHAT",前端传过来的是"wechat",一查就查不到,返回 null,后面链式调用直接空指针。对策分两层:

第一层,注册表入口统一兜底,查不到策略就抛出语义明确的异常:

public PayStrategy getStrategy(String channel) { PayStrategy strategy = strategyMap.get(channel); if (strategy == null) { throw new BusinessException("无可用支付策略:" + channel); } return strategy; }

第二层,策略 key 设计成枚举或常量,比如定义PayChannelEnum,用枚举的name()或code()做 key,避免字符串硬编码。如果历史原因导致前后端传参不一致,可以在注册时给同一个策略注册多个别名。

5.3 多个策略类同时被Spring注入时的顺序问题

用 List 接收 Spring 容器里所有策略 Bean 的时候,顺序是不保证的。如果你的业务在遍历策略时依赖顺序(比如"优先使用余额支付,余额不足再用信用咔"),一定要给策略加上@Order注解,或者在注册表里做显式排序:

@Component public class PayStrategyRegistry { private final List<PayStrategy> strategies; public PayStrategyRegistry(List<PayStrategy> strategies) { this.strategies = strategies; } @PostConstruct public void sort() { strategies.sort(Comparator.comparingInt(strategy -> strategy.getOrder())); } }

策略接口里加一个getOrder()或者在实现类上标@Order(1)都可以,关键是把顺序显式化,别让代码行为取决于 Spring 扫描类的偶然顺序。这种问题在测试环境可能十天半月不出现,上线后一到高并发就可能偶发,特别让人头大。

5.4 场景陷阱:什么时候不该用策略模式

最后聊一个很反直觉的问题:策略模式不是万能药。如果一个分支只有两种可能,且两种业务逻辑高度耦合,硬上策略模式反而会让代码变碎、类变多,阅读成本不降反升。

举个简单的例子,一个导出接口里根据文件类型xlsx和csv走不同的导出逻辑,但两种格式的差异只是几行代码,你完全可以用两个分支加一个公共方法解决。这时候强行抽一个ExportStrategy接口、写两个策略类、再弄一个注册表,就是在给代码增加无谓的复杂度。

判断标准很简单:把分支抽象成策略后,代码总量是否真的减少、新增一种类型时是否真的轻松、新人接手是否能更快看懂。三个条件满足两个以上,策略模式才值得上。否则,宁可保持 if-else,至少它简单直白。

6. 策略模式重构落地的一些私人心得

最后分享几个我在实际项目里总结的体会,不算完整方法论,但确实能提高策略模式的重构成功率。

第一个心得是,不要为了上策略模式而重构。如果那段 if-else 代码正在稳定运行、几乎没有新需求,别动它。重构是有成本的,策略模式最大的价值在于"预留扩展性"和"提升可测试性",这两点在业务快速迭代时收益最明显,在稳定系统里反而容易变成过度设计。

第二个心得是,模式落地时从最小闭环开始。不要一开始就铺开设计注册表、工厂、模板方法那一整套,先写一个最朴素的策略接口加两个实现类,跑通流程后再逐步加注册表、加 Spring 注入、抽公共父类。我见过团队一上来就搭一个复杂的分层框架,最后代码写了一千行,业务核心还没进去,新人直接劝退。

第三个心得是关于代码评审的:在代码评审时看到策略模式时,我一般会问三个问题。策略接口的方法是否足够抽象?有没有策略实现了多余的方法或者抛不支持异常?策略类的边界是否清晰,会不会把别家的参数校验写进自己类里?这三个问题能很快筛掉那些"为了模式而模式"的写法。

第四个心得也是最后一个:设计模式不是目的,可维护的代码才是。策略模式教会我们的不只是几个类之间的关系,更是一种思维方式——面对变化时,能不能把变化点隔离出来,让新增变得更容易,让修改变得更安全。这种思维在任何一种编程语言里都适用,Java 只是恰好给了你最标准的语法载体。

你在项目里写出第一个策略类的时候,如果明显感觉到"下次加需求我不用动老代码了",那这个模式你就真的用对了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询