资讯动态

策略模式实战:优雅消除if-else,Spring落地全攻略

发布时间:2026/10/22 3:09:22 来源:尧图企业网站定制
1. 从一段“烂代码”说起先说个我早期做项目时遇到的真实场景。某个同事负责一个后端接口里面有一段“经典”的业务逻辑根据用户类型发放不同力度的优惠券。代码长这样if (new_user.equals(userType)) { // 新用户注册礼包满99减30 } else if (vip.equals(userType)) { // VIP会员专享满199减80 } else if (normal.equals(userType)) { // 普通用户满299减20 } else if (invited.equals(userType)) { // 被邀请用户满99减10 } else { // 默认无优惠 }你可能觉得这没啥问题但实际情况是业务方隔三差五加一种用户类型每次都要往这个方法里塞一段if。过了两三个月这个方法膨胀到一百多行看着就头大。更要命的是测试同事每次都要重新把整个方法的逻辑过一遍生怕新增分支影响老逻辑。项目组里的每个人都知道这代码“有病”但没人愿意动因为太容易改坏了。这就是典型的“面向过程式分支”写法它的问题不在于if本身而在于它把“判断”和“执行”绑死在了一起。每增加一种策略就要修改旧代码这违反了设计原则里非常重要的一条——开闭原则对扩展开放对修改关闭。解药就是标题里说的这个老朋友策略模式。这篇文章我会用最朴素的语言从概念讲到实战再附上我在项目里踩过的坑和总结的一些代码写法上的习惯希望能帮你彻底把它用起来而不是停留在背定义、画 UML 图的表面阶段。这篇文章非常适合三类读者刚接触设计模式、对策略模式只停留在“好像听过这个名词”的初学者已经能看懂策略模式 Demo但不知道在 Spring 项目里怎么落地的新人被一堆if-else缠身、想重构又不敢动手的开发者。2. 策略模式到底在说什么2.1 一个生活化的类比咱们先不聊代码聊个生活场景。想象你开了一家饺子店店里的厨师分工明确张三擅长擀皮李四擅长调馅王五擅长煮饺子。顾客下单后店长不会把所有活儿都自己干而是根据订单内容把任务分配给对应的师傅。这里每个师傅就是一种“策略”店长只要做到“调度”他自己不需要会擀皮也不需要会调馅。以后如果来了个赵六擅长煎饺子店长只需要新增一个岗位不用把全店的工作流程改一遍。策略模式的核心思想就是这么简单把一组可互相替换的算法或业务规则封装起来客户端通过一个统一的入口去调用至于具体用哪个实现在运行时决定。2.2 三个核心角色陌生名词最容易劝退小白。咱们先记三个角色一切代码都只是这三个角色的具象化。角色职责类比策略接口Strategy定义一组算法的统一规范通常是一个接口饺子店里的“厨师”岗位说明书具体策略Concrete Strategy实现策略接口各自完成一种具体算法张三、李四、王五上下文Context持有策略接口的引用对外提供统一的调用入口店长负责调派人手很多教材里还会提到“客户端”Client它负责创建具体策略对象并设置到上下文中。在 Spring 项目里通常由容器帮我们完成这部分工作。2.3 和状态模式的区别很多初学者会把策略模式和状态模式搞混因为它们的类图长得有点像。我在这里简单区分一下后面不再赘述策略模式的“策略”是由客户端主动选择的客户端清楚知道自己要什么规则状态模式的“状态”是对象内部自己流转的外部并不关心对象当前处于什么状态只关心它对外表现的行为。打个比方策略模式像你用导航软件选“最快路线”还是“少收费路线”是你决定的状态模式像手机根据你当前所处位置自动切换“响铃”“振动”“静音”是设备自己判断的。这个区别在实际项目中很重要因为一旦选错模式代码会变得非常别扭。这一点我会在本文最后的实操心得里再展开讲一讲。3. 手写一个最最简单的 Demo“Talk is cheap.” 咱们直接上手写代码。这个 Demo 不依赖任何框架只用 JDK 自带的能力保证你能独立跑通。3.1 定义策略接口目标是实现一个计算器支持加法、减法、乘法。先定义一个统一的策略接口public interface CalculateStrategy { int calculate(int a, int b); }就这么简单。这个接口就是整个策略模式的核心契约你输入两个数我还你一个数。3.2 写三个具体策略加法策略public class AddStrategy implements CalculateStrategy { Override public int calculate(int a, int b) { return a b; } }减法策略public class SubtractStrategy implements CalculateStrategy { Override public int calculate(int a, int b) { return a - b; } }乘法策略public class MultiplyStrategy implements CalculateStrategy { Override public int calculate(int a, int b) { return a * b; } }每个策略类只做一件事所以它们都非常短。这不是偷懒这正是策略模式的设计目标每个策略足够内聚单一职责。将来要增加除法直接新增一个 DivideStrategy 类不影响任何旧代码。3.3 写上下文上下文是为了屏蔽客户端与具体策略实现类的耦合。没有上下文调用方就得自己 new 一个策略类然后直接调calculate这也不是不行但调用方就和具体实现绑定了失去了策略“可替换”的意义。public class CalculatorContext { private CalculateStrategy strategy; // 通过构造方法注入策略 public CalculatorContext(CalculateStrategy strategy) { this.strategy strategy; } // 通过 setter 方法注入策略可实现运行时动态切换 public void setStrategy(CalculateStrategy strategy) { this.strategy strategy; } public int execute(int a, int b) { return strategy.calculate(a, b); } }注意我同时保留了构造器注入和 setter 注入两种方式这在某些动态场景下非常有用。比如你的程序在运行过程中根据用户的交互行为改变计算规则setter 注入就是唯一的解。3.4 客户端调用public class Client { public static void main(String[] args) { CalculatorContext context new CalculatorContext(new AddStrategy()); System.out.println(3 5 context.execute(3, 5)); // 运行时切换策略 context.setStrategy(new MultiplyStrategy()); System.out.println(3 * 5 context.execute(3, 5)); } }跑一下输出如下3 5 8 3 * 5 15这个 Demo 虽然简单但已经完整展示了策略模式的三个核心动作定义统一接口、编写多个实现、由上下文对外提供服务。如果你只是想入门看到这里其实就够了。但现实项目里我们几乎不会这么“裸用”策略模式而是会结合 Spring 容器让它发挥更大的威力。下面进入正题。4. 在 Spring 项目里落地的正确姿势4.1 为什么不用 new上面的 Demo 中你看到了new AddStrategy()这种写法。在真实项目中我们基本不会直接在业务代码里new策略对象原因有两个对象生命周期管理混乱策略类大多是无状态的不持有成员变量或只持有不可变配置频繁创建和销毁没有意义交给 Spring 管理成单例更合理调用方仍然依赖具体类虽然CalculatorContext解耦了但如果客户端还要new实际上策略的选择逻辑还是散落各处。我们的目标是让调用方根本感觉不到策略类的存在传入一个类型标识自动完成分发。4.2 用 Spring 管理策略族先定义策略接口这次我们拿真实的业务场景来说——会员订单折扣计算public interface DiscountStrategy { // 返回这个策略对应的用户类型或场景标识 String getUserType(); // 根据原价计算折扣后价格 BigDecimal calculateDiscount(BigDecimal originalPrice); }接口里加了一个getUserType()方法为什么要加因为下面要用它和 Spring 的 Bean 名称/集合特性配合实现自动路由。实现三个策略类并注册为 Spring BeanComponent public class NewUserDiscountStrategy implements DiscountStrategy { Override public String getUserType() { return new_user; } Override public BigDecimal calculateDiscount(BigDecimal originalPrice) { // 新用户统一打 8.5 折 return originalPrice.multiply(new BigDecimal(0.85)); } }Component public class VipDiscountStrategy implements DiscountStrategy { Override public String getUserType() { return vip; } Override public BigDecimal calculateDiscount(BigDecimal originalPrice) { // VIP 用户满 199 减 80不满则原价 if (originalPrice.compareTo(new BigDecimal(199)) 0) { return originalPrice.subtract(new BigDecimal(80)); } return originalPrice; } }Component public class DefaultDiscountStrategy implements DiscountStrategy { Override public String getUserType() { return default; } Override public BigDecimal calculateDiscount(BigDecimal originalPrice) { // 默认用户不打折 return originalPrice; } }一句话总结这三个类getUserType()是策略的“门牌号”calculateDiscount()是策略的“实际动作”。4.3 核心利用 Spring 自动注入策略 Map这是整个实战中最关键、也最让人眼前一亮的地方。Spring 支持将某个接口的所有实现类自动收集到一个Map中Map 的 key 就是 Bean 名称。但我们不会直接用 Bean 名称作为路由因为 Bean 默认名称是类名首字母小写比如newUserDiscountStrategy不够直观、也不够稳定。所以我们需要做一个“别名映射”。先看代码Service public class DiscountStrategyContext { private final MapString, DiscountStrategy strategyMap; // Spring 自动将策略接口的所有实现类注入到这个 Map 中 public DiscountStrategyContext(MapString, DiscountStrategy strategyMap) { this.strategyMap strategyMap; } /** * 根据用户类型获取对应策略 */ public DiscountStrategy getStrategy(String userType) { // 重新构建映射关系策略标识 - 策略对象 return strategyMap.values().stream() .filter(strategy - strategy.getUserType().equals(userType)) .findFirst() .orElse(strategyMap.get(defaultDiscountStrategy)); } public BigDecimal execute(String userType, BigDecimal originalPrice) { DiscountStrategy strategy getStrategy(userType); return strategy.calculateDiscount(originalPrice); } }这段代码干了什么我拆解一下MapString, DiscountStrategy strategyMapSpring 看到构造器参数是一个 Map并且泛型 key 是 String会自动把容器中所有DiscountStrategy类型 Bean 收集进来key 是 Bean 名称getStrategy遍历这个 Map根据每个策略自己声明的getUserType()找到匹配项orElse兜底找不到对应策略时回退到默认策略defaultDiscountStrategy避免空指针。这里有小伙伴会问为什么不直接用 Bean 的名称作为 key因为 Bean 名称是约定俗成的类名首字母小写换个人重构类名就全乱了。而getUserType()是策略自己声明出来的标识业务语义更强也更稳定。4.4 controller 层调用RestController RequestMapping(/order) public class OrderController { private final DiscountStrategyContext strategyContext; public OrderController(DiscountStrategyContext strategyContext) { this.strategyContext strategyContext; } PostMapping(/price) public BigDecimal calculate(RequestBody OrderRequest request) { return strategyContext.execute(request.getUserType(), request.getOriginalPrice()); } }调用方传入什么用户类型内部就自动路由到对应策略。业务层不需要出现任何if。注意这个版本用的是 Java 8 的 Stream 遍历性能完全够用因为策略数量通常是个位数到十几个不是性能瓶颈。如果你用的是更现代的写法比如 Java 8 的Map.merge或者直接构造一个HashMap手动填充也是可以的。但上面这种基于 Spring 自动注入的风格最干净也最符合主流项目的习惯。4.5 直接在 Map 上做逻辑的优化空间有经验的开发可能已经发现每次调用getStrategy时都要遍历整个 Map不够优雅。虽然量小无所谓但我们可以做一个“更聪明”的初始化Service public class DiscountStrategyContext { private final MapString, DiscountStrategy strategyMap new HashMap(); // Spring 会把所有实现注入到 list 中 public DiscountStrategyContext(ListDiscountStrategy strategies) { // 在构造阶段就建立好路由表 for (DiscountStrategy strategy : strategies) { this.strategyMap.put(strategy.getUserType(), strategy); } } public DiscountStrategy getStrategy(String userType) { return strategyMap.getOrDefault(userType, strategyMap.get(default)); } public BigDecimal execute(String userType, BigDecimal originalPrice) { DiscountStrategy strategy getStrategy(userType); return strategy.calculateDiscount(originalPrice); } }把“路由表的构建”放在构造函数里完成之后每次查询都是 O(1) 的 HashMap 查找。这种写法还解决了一个隐患如果有两个策略返回了相同的getUserType()在构建阶段就会覆盖掉前面的值你很快就能发现问题或者也可以用检测重复的代码提前报错而不是等到调用时才出现诡异行为。我给个小建议如果策略数量比较少用 Stream 遍历即可代码更直观如果策略种类比较多或者对性能有轻微要求用第二种“构建路由表”的方式差别其实也不大。但无论如何别在设计上把策略标识写死成“约定”一定要让每个策略自己报告身份这样以后加策略才真正省事。5. 策略模式 枚举与函数式接口的“现代玩法”如果你觉得上面的 Spring 装配方案还不够过瘾我再介绍一种近两年在项目里越来越常见的写法策略模式结合枚举和Function接口。这种写法特别适合那种“策略体量小、一个方法就搞定”的场景能把代码量压缩到极致同时不失可读性。public enum DiscountStrategyEnum { NEW_USER(new_user, price - price.multiply(new BigDecimal(0.85))), VIP(vip, price - price.compareTo(new BigDecimal(199)) 0 ? price.subtract(new BigDecimal(80)) : price), DEFAULT(default, price - price); private final String userType; private final FunctionBigDecimal, BigDecimal handler; DiscountStrategyEnum(String userType, FunctionBigDecimal, BigDecimal handler) { this.userType userType; this.handler handler; } public static BigDecimal execute(String userType, BigDecimal originalPrice) { for (DiscountStrategyEnum strategy : values()) { if (strategy.userType.equals(userType)) { return strategy.handler.apply(originalPrice); } } return DEFAULT.handler.apply(originalPrice); } }调用BigDecimal price DiscountStrategyEnum.execute(vip, new BigDecimal(230));这种写法本质上是“策略模式 枚举 lambda”的组合拳三个角色的代码全部收拢到一个枚举里。它的最大优势是代码集中、一眼就能看出所有策略全貌最大劣势是策略之间如果有大量共享逻辑就不容易复用。所以我的建议是策略方法体只有一两行的优先用枚举 lambda 就够了策略方法体超过十行、或需要依赖多个服务组件的用 Spring Bean 上下文的完整策略模式。两者不冲突甚至可以混合使用。好的设计从来不是“只用一种模式”而是“在最合适的位置用最合适的方式”。6. 常见问题与排查技巧实录这块内容是我的“独家干货担当”。下面这些问题全部来自真实开发过程中的现身说法有的问题不是看两篇博客就能知道的。6.1 Spring 注入 Map 后 Bean 名称对不上的问题新手容易踩的第一个坑Map 注入后key 是 Bean 名称没错但当你用自定义的getUserType()去查找时发现查不到。原因一般有两个类上没有加Component/ServiceSpring 容器里根本没有这个 BeanMap 里自然是空的getUserType()返回的字符串和传入的userType不一致比如多了一个空格、大小写不同。排查方法可以在DiscountStrategyContext的构造函数中加一个断点或者干脆在启动的时候打日志把 Map 的 key 和每个策略上报的getUserType()打出来一览无遗。让“锅”浮出水面别靠肉眼去读代码。6.2 多个策略类导致的无用类数量膨胀这是“过度设计”的典型反面例子。有些团队为了用设计模式而用设计模式散弹枪式地创建七八个策略类每个类就写了一行 return抽象过度反而让新人看不懂。解决办法评估策略的复用性和复杂度。如果一个策略只有一行 return且没有多个地方复用那这个策略就没有独立成类的必要用枚举或直接在调用处做条件判断更合适。策略模式的意义在于“算法族的可替换性”不是“消灭所有的 if”。有些 if 是业务状态的合法映射不该被消灭。我个人的判断标准比较主观一个策略类的方法体低于三行且策略总数不超过三个我就会考虑是否真的要用完整策略模式。重模式的收益主要在长期维护和扩展上但短期需要付出一定的阅读成本。6.3 策略里塞了太多代码变成“上帝策略”还有另一种反面案例策略类数量虽然不多但某个策略类的方法体写了两百行把所有特殊情况都堆在里头。这其实就不是策略的问题了而是“策略内部分支过多”。处理思路是如果一个策略太复杂应继续拆分成子策略或者用模板方法模式固定主流程把可变部分继续向下抽象。策略模式可以嵌套使用这不是什么丢人的事。嵌套时注意保持每个子策略的命名清晰别最后自己都找不到北。6.4 运行时动态切换策略为什么不生效有些同学在 controller 里先getStrategy拿到了策略对象之后想换策略发现换不掉。原因是策略上下文持有的是策略对象的副本而你改的是另一个引用。这种问题通常出在没有通过上下文更新策略而是直接改了局部变量或者策略被 Spring 配置成了单例但你在策略内部不小心写了可变的成员变量。我的建议策略类尽量设计成无状态的不持有可变成员变量。如果确实需要携带配置参数可以用Value注入只读配置但不要在策略类里声明可变的HashMap之类的东西。多线程环境下这会造成非常隐蔽的 bug。6.5 策略内部需要用 Spring 管理的其他 Bean策略类也是 Bean所以可以直接通过构造器把需要的其他组件注入进来Component public class VipDiscountStrategy implements DiscountStrategy { private final CouponService couponService; public VipDiscountStrategy(CouponService couponService) { this.couponService couponService; } Override public BigDecimal calculateDiscount(BigDecimal originalPrice) { BigDecimal vipDiscount originalPrice.subtract(new BigDecimal(80)); // 还可以叠加优惠券 return couponService.applyCoupon(vipDiscount, VIP_COUPON); } }这正好演示了策略模式的“可组合性”具体策略内部完全可以调用其它服务它的职责只是“算折扣”至于折扣计算过程中需要查表、校验、准备数据是它自己的事。7. 策略模式背后的设计思想很多初学者困惑策略模式就这么简单为什么那么多资深前辈强调它的价值我理解策略模式真正想解决的不是“消除 if-else”这个问题本身而是让代码的可变部分和不可变部分相互隔离不可变部分调用的入口getStrategy、路由方式Map 映射、默认兜底逻辑可变部分每个策略内部的业务规则。这种隔离带来了三个长期收益可测试性高每个策略可以独立写单元测试不需要启动整个 Spring Boot 应用也不需要 mock 一堆不相关的类变更成本低新增一个策略只需新增一个类不改动任何现有类代码评审时 diff 极小回归测试的范围也极小新人友好新同学接手时只需要看DiscountStrategyContext和策略接口就能理解整个规则体系的运作方式不需要在一个几百行的 if-else 里考古。还有人会问这和“面向接口编程”有什么区别区别就是策略模式把“用接口来调用算法”和“算法的运行时选择”两者统一了起来。面向接口编程是基本功而策略模式是基本功在具体业务场景中的一种经典编排方式。8. 工具选型与代码评审时的关注点这一节回答一个很多人关心的问题策略模式在代码评审中出现时评审者一般会看什么也能帮你自查。关注点一策略标识是否稳定。getUserType()这类标识字段不应该取自用户可修改的原始输入而应该来自后端内部定义好的枚举或字典。否则用户传什么值策略就随便命中安全性带隐患。关注点二兜底策略是否存在。永远留一个 default 策略因为路由查不到的情况一定会出现与其让代码报空指针不如让它走一个明确的默认行为。关注点三策略之间是否存在隐式耦合。有些策略可能依赖“前一个策略已经执行过”的上下文状态这种隐式耦合一旦出现策略就退化成了一种顺序执行的 paint 流程不再是独立的算法族了。这时候需要重新梳理一下拆分粒度和调用时序。关注点四Spring 的注入写法是否干净。见过有人在策略 Map 的获取上又包了一层Autowired字段注入混合了字段注入和构造器注入风格代码虽能运行但一致性很差后续维护会增加心理负担。建议全项目统一用构造器注入。9. 策略模式在真实项目中的一个完整场景还原我常喜欢把知识放在一个完整的场景里去验证。这里我构造一个尽量贴近实际业务需求的小项目把它从头到脚走一遍最终代码的结构你可以直接“抄”。场景某商城支持“多支付方式结算”。不同支付方式的手续费率、封顶金额、打折系数不同。支付方式包括余额支付、微信支付、支付宝支付、银行卡支付。每种方式的计费规则如下支付方式计费规则余额支付手续费率为 0微信支付手续费率为 0.6%最高 20 元支付宝支付手续费率为 1%最高 25 元银行卡支付手续费率为 1.2%最高 50 元这个场景非常适合策略模式因为支付方式会不断增加比如后续扩展“数字人民币支付”每种支付方式的费率算法互相独立、互不干扰调用的入口是统一的服务外部只需要传入支付方式标识。接口public interface PaymentFeeStrategy { String getPaymentType(); BigDecimal calculateFee(BigDecimal amount); }实现类之一Component public class WechatPayFeeStrategy implements PaymentFeeStrategy { Value(${pay.wechat.rate:0.006}) private BigDecimal rate; Value(${pay.wechat.cap:20}) private BigDecimal cap; Override public String getPaymentType() { return wechat; } Override public BigDecimal calculateFee(BigDecimal amount) { BigDecimal fee amount.multiply(rate); return fee.min(cap); } }其它支付方式类似。然后上下文Service public class PaymentFeeContext { private final MapString, PaymentFeeStrategy strategyMap; public PaymentFeeContext(ListPaymentFeeStrategy strategies) { strategyMap new HashMap(); for (PaymentFeeStrategy strategy : strategies) { strategyMap.put(strategy.getPaymentType(), strategy); } } public BigDecimal calculateFee(String paymentType, BigDecimal amount) { PaymentFeeStrategy strategy strategyMap.get(paymentType); if (strategy null) { throw new IllegalArgumentException(Unsupported payment type: paymentType); } return strategy.calculateFee(amount); } }这里我故意没有加兜底策略而是直接抛出异常。为什么因为支付方式不支持时是硬错误不应该是默认行为这和用户折扣场景的“默认策略”语义不同。这个细节说明一个道理模式是骨架业务语义才是血肉不能机械套用。测试类可以这样写SpringBootTest class PaymentFeeContextTest { Autowired private PaymentFeeContext context; Test void wechatFee() { BigDecimal fee context.calculateFee(wechat, new BigDecimal(100)); // 100 * 0.006 0.6 assertEquals(new BigDecimal(0.6), fee); } Test void wechatFeeCap() { BigDecimal fee context.calculateFee(wechat, new BigDecimal(5000)); // 5000 * 0.006 30 - 封顶 20 assertEquals(new BigDecimal(20), fee); } }这套结构放回真实项目里无论是扩展新的支付方式还是调整某一种支付方式的费率都只需要动很小的一块环境完全符合开闭原则。10. 策略模式常见误用自查清单现象问题根源该怎么做策略类数量爆炸每个策略就一行代码过度抽象考虑用枚举 函数式接口替代上下文里出现大量 if-else路由逻辑没抽象好用 Map 构建路由表替代策略类里持有可变状态线程安全风险改成无状态或使用局部变量调用方频繁新建策略对象没有利用 Spring 容器交给 Spring 管理成单例策略标识写死在字符串里维护困难易脏数据定义枚举常量统一管理不同策略间有顺序依赖拆分粒度不当将顺序逻辑放进编排层而不是策略内部这张表建议收藏每次自测代码时对照一遍可以帮你避开大部分设计层面的坑。11. 我在团队里推行策略模式时的三个心得代码说完了最后聊点软性的东西。第一警惕“模式化改造强迫症”。我曾经在一个项目里看到某个同学把一段三行的 if-else 硬生生套上策略模式额外产生了四个类。这种改造除了让类图变得花哨之外没有带来任何实际收益。先问自己三个问题这段逻辑是否频繁变更算法是否互相独立调用方是否需要动态切换行为三个问题答案只要不是“是”就别动刀。第二策略模式的蔓延速度比想象中快。一旦你落地了一处业务方尝到了“加功能不回归”的甜头很快就会在更多场景复制这个模式。这本来是好事但要注意控制策略粒度的统一。我会和一个团队的成员约定一个简单规范策略接口的语义必须单一比如“计算折扣”和“计算运费”不要混在同一个接口里。第三文档要跟着策略实现走。策略类一多如果没有人维护一张“策略 - 标识 - 生效规则”的速查表后续维护者很快就晕了。我把这张表放在代码仓库的 markdown 文件里每次新增策略时要求同时更新速查表。这不是强制规范但做了几次之后大家都发现可以少问很多“这个策略是干嘛的”的重复问题。最后再分享一个小技巧在策略类比较多的时候可以在类名上用业务前缀统一风格比如OrderDiscountStrategy、OrderShippingStrategy这样通过 IDE 的类名搜索就能快速定位到指定领域中所有可用的策略比直接改Map里的 key 要直观得多。希望这篇从概念到实战、从代码到心法的长文能让你彻底拿捏策略模式。下次再看到那种几十个分支的 if-else 时你就有底气把它优雅地重构掉了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑