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 ListPayStrategy strategies; public PayContext(ListPayStrategy 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 MapString, PayStrategy strategyMap; public PayContext(MapString, 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) { ListPayStrategy 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再转成 MapComponent public class PayStrategyRegistry { private final MapString, PayStrategy strategyMap new HashMap(); public PayStrategyRegistry(ListPayStrategy 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 FunctionBigDecimal, BigDecimal calculator; MoviePriceStrategy(String description, FunctionBigDecimal, 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 ListPayStrategy strategies; public PayStrategyRegistry(ListPayStrategy 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 只是恰好给了你最标准的语法载体。你在项目里写出第一个策略类的时候如果明显感觉到下次加需求我不用动老代码了那这个模式你就真的用对了。