策略模式实战:用Java和Spring告别if-else地狱 1. 先搞清楚一个问题策略模式到底解决的是什么先说个真实场景。我之前接手过一个电商订单模块刚开始代码写得很爽普通会员不打折黄金会员九折铂金会员八折于是方法里出现了三个 if。产品经理看完后表示很好然后第二周加了满减第三周加了新人券第四周加了店铺优惠券叠加第五周加了黑名单用户不走任何优惠……于是原来那个三行 if-else 的方法慢慢长成了一个“远古巨神”十几个分支互相嵌套后面的同事改计算规则时动一行就要看半天因为怕影响别的逻辑。最要命的不是代码长而是每次加一种玩法都得去动那段已经没人敢碰的老代码。这东西在业界有个很形象的名字if-else 地狱。而策略模式就是用来收拾这个局面的。1.1 策略模式的核心定义策略模式Strategy Pattern的定义网上有很多版本我用自己的话给你翻译一遍把一类可以互相替换的算法或规则各自封装成独立的对象然后在运行时动态地选择用哪一个。拆开来看就是三件事一是“算法”。这里的算法不一定是快排、动态规划那种高大上的东西它可以是“怎么计算价格”“怎么发送通知”“怎么解析文件”……凡是同一件事的不同做法都算。二是“封装成独立对象”。每一个做法不再散落在 if-else 分支里而是各成为一个小类单独维护、单独测试。三是“运行时动态选择”。写代码的时候不写死“用什么”而是给一个输入条件让代码自己去挑。打个比方你要从家里去公司交通工具可以是地铁、公交、打车。这三种方式就是三个策略。你出门前不需要把“每条路线怎么走”全部记住只需要告诉导航“我今天赶时间”导航就会自动帮你把打车这个策略挑出来。1.2 策略模式的三个角色策略模式一共有三个固定角色理解这三句话你就抓住它的骨架了策略接口Strategy定义一组算法的统一规范一般是一个接口或者抽象类。比如“怎么到达公司”这个接口里面声明一个方法go()。具体策略ConcreteStrategy实现策略接口的类每个类代表一种具体算法。比如MetroStrategy、BusStrategy、TaxiStrategy。上下文Context持有一个策略接口的引用负责把具体策略装配起来。它对外提供统一方法内心里却通过持有的策略对象去做真正的事。你不需要把这三个角色背下来后面代码一写自然就记住了。想提醒的是策略模式的精髓不在接口而在组合优于继承——上下文不继承任何策略它是“持有”一个策略然后在合适的时候调用它。1.3 什么时候该用什么时候别硬用策略模式不是装饰品更不是用来炫技的。我的经验是满足下面这几种特征的时候用它效果最好第一同一行为有多个算法或规则而且它们能在运行时切换。第二代码里出现了一坨看着就想吐的 if-else并且每个分支后面都是一段完整独立的逻辑。第三你预感到这块规则未来会频繁变化而且新规则总是像雨后春笋一样冒出来。反过来如果是那种“就两三种情况并且一辈子不会再变”的简单分支我强烈不建议你上策略模式。比如判断一个数是不是偶数你硬要写个EvenStrategy接口那纯属给自己加戏。模式是工具不是目的。2. 从零手写一个策略模式商城折扣计算实战光讲概念记不住直接上手写代码。这一节我们从最朴素的方式把策略模式完整实现一遍全程不需要任何框架纯 Java 就能跑。2.1 场景设定给订单价格计算做重构假设我们有这样一个需求订单价格计算分为普通用户、会员用户、VIP 用户三档后续还可能加满减、优惠券等玩法。没有用策略模式之前代码大概长这样public double calcPrice(double orderPrice, int userLevel) { if (userLevel 0) { return orderPrice; } else if (userLevel 1) { return orderPrice * 0.9; } else if (userLevel 2) { return orderPrice * 0.8; } return orderPrice; }这段代码有两个问题第一业务规则全堆在一个方法里每个分支越写越长第二想新增一种用户等级就要往这个方法里面再加一个 else-if老代码越改越飘。我们用策略模式重写。2.2 第一步定义策略接口策略接口做的事情很简单就是把“计算各种价格”这个行为抽象出来public interface DiscountStrategy { // 输入原始订单金额返回优惠后的金额 double apply(double originalPrice); }接口的方法名我用了apply意思就是“把策略应用上去”。名字不重要你可以叫discount、calculate都行关键是语义清晰。2.3 第二步实现具体策略类每个用户等级对应一个实现类互不干扰。先写普通用户public class NormalDiscountStrategy implements DiscountStrategy { Override public double apply(double originalPrice) { return originalPrice; } }会员用户public class MemberDiscountStrategy implements DiscountStrategy { Override public double apply(double originalPrice) { return originalPrice * 0.9; } }VIP 用户public class VipDiscountStrategy implements DiscountStrategy { Override public double apply(double originalPrice) { return originalPrice * 0.8; } }你发现没有这三个类之间没有任何联系各自维护自己的一条规则。以后要是来了个“超V”你不用碰这三个类直接新建一个SuperVipDiscountStrategy就完事了。这是策略模式最大的价值新增一种玩法不需要动已有的代码符合开闭原则。2.4 第三步用上下文把策略串起来有了策略谁来持有它、调用它答案是上下文类。它就像手机里的导航。public class OrderPriceContext { private DiscountStrategy strategy; // 运行时动态设置策略 public void setStrategy(DiscountStrategy strategy) { this.strategy strategy; } // 对外统一的计算入口 public double getFinalPrice(double originalPrice) { if (strategy null) { throw new IllegalStateException(请先设置优惠策略); } return strategy.apply(originalPrice); } }上下文不关心具体用的是哪个策略类它只知道手里有一个DiscountStrategy调用它的apply方法就能得到结果。有的同学可能会问这个上下文不是比 if-else 还麻烦吗别急客户端用起来你就知道好处了。2.5 第四步客户端调用打个比方订单接口接收到前端传来的用户等级然后决定用哪种策略public class OrderService { private OrderPriceContext context new OrderPriceContext(); public double calculate(double price, int userLevel) { if (userLevel 0) { context.setStrategy(new NormalDiscountStrategy()); } else if (userLevel 1) { context.setStrategy(new MemberDiscountStrategy()); } else if (userLevel 2) { context.setStrategy(new VipDiscountStrategy()); } return context.getFinalPrice(price); } }到这一步你可能还是觉得 if-else 还在。你说得对选择哪个策略这件事本身还是需要某种逻辑的。这里我想解释清楚一个容易混淆的点策略模式不等于消除所有 if-else它消除的是“算法实现”里的 if-else而“策略选择”这个过程可以用工厂、枚举、配置表来优化。不过哪怕是当前这种写法好处也已经非常明显每个策略内部不再有互相嵌套的分支你可以把VipDiscountStrategy单独拿出来做单元测试测试它打八折是否正确。在真实项目里这个可变现的价值比那点 if-else 重要得多。3. 项目实战Spring 环境下的策略模式集合法如果只是写 Demo上面的代码已经足够了。但现实中的 Java 项目十有八九离不开 Spring。这一节我们谈谈在 Spring Boot 项目中怎么让策略模式用得更顺手。3.1 场景升级多渠道支付以“订单支付渠道选择”为例。用户下单后可以选择支付宝、微信支付、银行卡支付。不下单的时候代码不知道用户会选哪个渠道只有运行到那一步才知道。而且支付渠道这个业务天生就是“加一个渠道就多一个实现”相当适合策略模式。先定义策略接口public interface PaymentStrategy { // 每种策略标识自己的渠道名 String channel(); // 执行支付 void pay(PayRequest request); }注意我加了一个channel()方法专门用来标记当前策略是哪个渠道。这是策略模式在实际项目里的常见改进后面选择策略时会很有用。3.2 编写具体的支付策略这三个实现类放到 Spring 容器里分别打上Component注解Component public class AlipayStrategy implements PaymentStrategy { Override public String channel() { return alipay; } Override public void pay(PayRequest request) { // 调用支付宝的接口 System.out.println(使用支付宝支付 request.getAmount()); } }微信支付Component public class WechatPayStrategy implements PaymentStrategy { Override public String channel() { return wechat; } Override public void pay(PayRequest request) { // 调用微信支付的接口 System.out.println(使用微信支付 request.getAmount()); } }银行卡支付Component public class BankCardPayStrategy implements PaymentStrategy { Override public String channel() { return bankcard; } Override public void pay(PayRequest request) { // 调用银行卡支付接口 System.out.println(使用银行卡支付 request.getAmount()); } }到这里都是常规操作关键在下一步怎么把这些策略统一收拢到一个入口里。3.3 利用 Spring 自动注入策略 MapSpring 有一个非常方便的特性如果注入一个MapString, 某个接口类型Spring 会自动把容器中所有该接口的实现类都放进去key 是 Bean 的名称。我们可以利用这个特性写一个策略分发器Service public class PaymentDispatchService { private final MapString, PaymentStrategy strategyMap; // Spring 会自动把 AlipayStrategy、WechatPayStrategy 等注入到这个 Map 中 public PaymentDispatchService(MapString, PaymentStrategy strategyMap) { this.strategyMap strategyMap; } public void pay(String channel, PayRequest request) { PaymentStrategy strategy strategyMap.get(channel); if (strategy null) { throw new UnsupportedOperationException(不支持的支付渠道: channel); } strategy.pay(request); } }眼尖的同学会发现这个 Map 的 key 默认是 Bean 的名字也就是类名首字母小写alipayStrategy、wechatPayStrategy。它和我们策略实现里的channel()方法并不完全一致。那怎么处理呢有几个方案方案一Bean 名字直接用渠道名。就是把Component(alipay)写成这样。但类名和 Bean 名不一致稍微有点绕。方案二在策略接口里加channel()然后在注入完成后用 Bean 名字去定位对应的策略对象把 Map 重新组装一遍。这样虽然多一点代码但不用依赖 Bean 的命名规则。方案三也是我比较常用的直接用枚举做渠道定义让channel()返回枚举值。选策略的时候用枚举作为 key。这里不追求标准答案核心点是Spring 自己把你需要的策略实现类聚成一个 Map这大大简化了“策略选择”的装配过程。你不必手动 new 一堆策略对象也不需要写一个地方专门注册它们。以后新增一个支付渠道直接写一个类加Component即可PaymentDispatchService一行不用改。3.4 业务层调用一行代码搞定分发有了PaymentDispatchService上层业务调用起来非常干净Service public class OrderService { private final PaymentDispatchService paymentDispatchService; public OrderService(PaymentDispatchService paymentDispatchService) { this.paymentDispatchService paymentDispatchService; } public void createOrder(OrderDTO order) { // 省略订单保存逻辑 paymentDispatchService.pay(order.getChannel(), order.toPayRequest()); } }这段代码身上没有任何“渠道判断”它只负责把用户选择的渠道字符串透传下去。以后新增渠道这个类一行都不用动非常舒服。我自己在项目里用了这种结构之后最直观的感受是以后排期里面写“新增一个支付渠道”任务量非常可控基本就是写一个策略类 写两行测试。这种可预测的工作量在排期和评估时特别有价值。4. 项目里的策略模式常见问题与排查实录用了这么久策略模式我也没少踩坑。这里把大多数人在项目中会遇到的问题整理一下有些问题是代码层面的有些是设计层面的。4.1 问题速查表先把高频问题列成一张表方便你对照排查。现象根本原因解决方案策略类越来越多管理混乱粒度拆分过细每个细微差别都搞一个类按业务维度收敛或改用函数式接口新增策略后线上却不生效策略没有被容器扫描到或选择器没注册检查包扫描路径检查选择器获取逻辑多个策略里有大量重复代码共性代码没有被抽取用抽象父类或模板方法提取公共部分策略对象里到处都是状态字段把有状态的对象当成策略类用了策略尽量设计成无状态单例不知道选哪个策略选择器里的 if-else 又膨胀了把“策略选择”也写死了用 Map 枚举 配置驱动来收敛选择逻辑策略模式重写后故障排查变难原先是集中式日志现在散落到各个类在上下文里增加明确日志与统一入口埋点4.2 最容易犯的错策略类和选择器一起膨胀很多人一开始兴致勃勃地重构了 if-else结果过了一段时间发现选择器里又长出了一坨新的 if-else。比如这样public PaymentStrategy choose(String channel) { if (alipay.equals(channel)) { return alipayStrategy; } else if (wechat.equals(channel)) { return wechatPayStrategy; } else if (bankcard.equals(channel)) { return bankCardPayStrategy; } throw new ...; }这其实是没理解“策略选择”和“策略实现”是两个维度的东西。策略模式管的是后者前者的职责应该交给一个注册表、配置表或者直接利用 Spring 自动注入得到的 Map。正确的做法是strategy strategyMap.get(channel);如果用一句话总结让选择器变成纯查表操作不要在表查询里写业务分支。4.3 策略对象的有状态问题有次排查一个诡异 Bug现象是同一个用户用 VIP 折扣计算价格第一次正确第二次价格就错了。找了好久才发现有人在写策略类的时候把“当前用户折扣结果”作为成员变量存在了类里面。这个用法很危险。因为策略对象在 Spring 容器里默认是单例多个线程同时执行时成员变量会发生并发竞争结果就是数据被互相覆盖。正确的姿势是策略实现类保持无状态。所有需要的数据都通过方法参数传进去不要用成员变量缓存结果。如果你确实需要某些上下文数据可以在调用时包装成一个对象传进方法。4.4 策略模式和工厂模式容易搞混这俩确实容易混因为它们经常一起出现。我的区分方法是工厂模式关心的是“怎么把对象创建出来”它帮你解决 new 哪个类的问题。策略模式关心的是“把算法插进去运行”它帮你解决运行期怎么替换实现的问题。实际项目里经常是这样的组合用工厂模式根据条件创建出具体的策略对象再用上下文持有这个策略对象并运行它。先有工厂选型再有策略执行两者不冲突也不该混为一谈。5. 策略模式的上手到进阶一些更省力的玩法和搭配思路到这里策略模式的原理和项目落地已经说通了。再往后走可以把眼光放更远一些看看真实项目里怎么把策略模式玩得更加顺手。5.1 用函数式接口给策略“减重”传统策略模式要写很多类虽然结构清晰但有时候会觉得“只为一句话的逻辑也建一个类”有点笨重。Java 8 的 Lambda 和方法引用可以把这个成本压低很多。比如折扣计算这个例子接口如果是一个函数式接口我们可以直接这样写public interface DiscountStrategy { double apply(double originalPrice); }使用时用 Map 把策略名和 Lambda 关联起来MapString, DiscountStrategy strategyMap new HashMap(); strategyMap.put(normal, price - price); strategyMap.put(member, price - price * 0.9); strategyMap.put(vip, price - price * 0.8); strategyMap.put(fullReduce, price - { // 满300减80 return price 300 ? price - 80 : price; });用的时候一行调用double result strategyMap.getOrDefault(vip, price - price).apply(100.0);这种方法特别适合内部接口较多、策略逻辑较短、不需要把每个策略都独立成类的场景。但我要提醒一句如果策略逻辑比较复杂或者多个开发者要长期维护我还是建议老老实实写类因为类的可读性和可测试性更好也更容易被团队熟悉。5.2 策略模式搭配模板方法模式公共流程统一的套路项目里经常会出现一种情况多个策略之间有共同的执行步骤只有中间某一步不一样。比如报表解析都是“读取数据 - 清洗数据 - 核心计算 - 导出结果”但不同报表的计算规则不一样。这时候把“公共流程”放到一个抽象父类里再把“变化的计算规则”定义为需要子类实现的抽象方法。这种做法叫模板方法模式它和策略模式的组合还挺经典的public abstract class AbstractReportParser { public void parse(ReportData data) { this.validate(data); // 选择策略计算 StatisticStrategy strategy this.selectStrategy(data.getType()); StatisticResult result strategy.calculate(data); this.output(result); } protected abstract void validate(ReportData data); protected abstract StatisticStrategy selectStrategy(String type); protected abstract void output(StatisticResult result); }模板方法把主流程定死策略模式把可变算法隔离。实际工程中这种“模板定骨架 策略填细节”的配合极其常见能省掉很大一批重复代码。5.3 说几个藏在 JDK 和框架里的策略模式其实你在平时开发中早就见过它了只是没有意识到。最典型的就是Comparator接口。Collections.sort(list, comparator)这个方法传入不同的比较器排序策略就不一样。按年龄排、按姓名排、按金额排都是同一个 sort 方法但它背后会根据策略对象执行完全不同的比较逻辑。再比如线程池的拒绝策略ThreadPoolExecutor可以设置四种拒绝策略AbortPolicy、CallerRunsPolicy、DiscardPolicy、DiscardOldestPolicy。这四种策略也是典型的策略模式应用线程池不关心你选了哪种它只负责在任务满了时候调用你设置的那个策略对象。下次写设计模式总结的时候你可以把这些现成的例子找出来聊比空谈理论有说服力得多。5.4 关于“谁选策略”的最终建议策略模式用得好不好很大程度取决于“谁在负责选择策略”。我见过三种方案第一种是把选择逻辑放在调用方也就是客户端传入渠道名符合直觉局部清晰。这是最常见也是我推荐起步使用的方案。第二种是上下文内部做选择调用方传入一个条件上下文自己去查表。这种方案对调用方更友好但上下文会变得略胖。第三种是使用注册中心通过注解和反射把策略自动注册进 Map。大型框架里常见但普通业务项目没必要搞这么重。我只强调一条原则选择逻辑所在的层越靠近调用入口后续维护的人越容易看懂。别让选择逻辑散得到处都是也别让策略类自己跳出来“选自己”。最后说点实在话我最初接触策略模式时总觉得它是学校里教出来的理论和真实业务的复杂程度不在一个量级。等到真正在一个需求天天变、规则层层叠的项目里把它用起来才明白它最大的价值其实不是让代码变“高级”而是让变动变成局部的、可预期的、甚至有点无聊的动作加一个类往 Mapp 里注册完事。如果你现在手头正有一个长满 if-else 的方法我强烈建议今天就去试试策略模式。不用一步到位就先把最复杂的三个分支抽成三个类看看代码结构的变化感受一下替换成本。试完之后你会发现设计模式这个东西确实是一分理解一分收益。