Java策略模式实战:Spring Bean、枚举与Lambda三种实现与选型 1. 从if-else地狱说起策略模式到底在解决什么问题做Java后端这几年我拆过太多堆满if-else的业务类了。尤其是支付、通知、营销这类场景每来一个新渠道、新玩法就往老代码里塞一个分支。刚开始还能忍等分支超过七八个整个方法就成了一锅粥——读起来费劲改起来胆战心惊测试用例越堆越多却还是怕漏掉某个组合。策略模式Strategy Pattern就是为了治这个病才被反复拿出来讨论的。它不是一个新东西GOF那本书里就讲得很清楚把一族可互换的算法或行为封装起来让客户端面向接口编程在运行时选择具体实现。但问题在于——Java的世界里封装一族实现这件事有很多种落地方式。Spring容器帮你管理Bean是一种枚举类型配合抽象方法是另一种Java 8之后的Lambda表达式又是一种。很多人知道策略模式的原理一写代码却只会用最传统的手写工厂加Map白白浪费了框架和语言给你的便利。这篇文章就围绕这三种实现路径展开Spring Bean怎么承载策略族、枚举怎么把策略焊死在类型系统里、Lambda怎么让少量且简单的策略代码瘦身。我会把三种方式的适用边界、坑点和选型逻辑都讲透最后用一个支付渠道的完整案例把三种方式串起来演示。适合正在做业务系统重构、想优化代码结构的Java开发者也适合那些读过策略模式理论、但不知道怎么和Spring结合的新人。2. Spring Bean策略把策略家族交给容器管理2.1 核心玩法一个Map搞定策略分发Spring管理策略模式最经典的姿势就是依赖注入一个MapString, 策略接口。Spring容器在启动的时候会把所有该类型的Bean收集起来Key是Bean名称Value是Bean实例。你不需要手写任何注册逻辑新增一个策略就是新增一个Component老代码一行都不用改。先看一个最基础的样子。假设我们在做一个订单折扣系统不同用户等级对应不同的折扣算法public interface DiscountStrategy { BigDecimal calculate(BigDecimal amount); String userLevel(); } Component(vip) public class VipDiscountStrategy implements DiscountStrategy { Override public BigDecimal calculate(BigDecimal amount) { return amount.multiply(new BigDecimal(0.8)); } Override public String userLevel() { return vip; } } Component(normal) public class NormalDiscountStrategy implements DiscountStrategy { Override public BigDecimal calculate(BigDecimal amount) { return amount; } Override public String userLevel() { return normal; } }然后在调用方直接注入MapService public class DiscountService { private final MapString, DiscountStrategy strategyMap; public DiscountService(MapString, DiscountStrategy strategyMap) { this.strategyMap strategyMap; } public BigDecimal applyDiscount(String userLevel, BigDecimal amount) { DiscountStrategy strategy strategyMap.get(userLevel); if (strategy null) { throw new IllegalArgumentException(unsupported userLevel: userLevel); } return strategy.calculate(amount); } }核心就两行一个Map注入一个get取用。你可能会觉得这跟手写工厂差不多区别在于——手写工厂里你要自己维护注册关系而Map注入完全依赖Spring扫描和聚合策略类只要存在容器就会自动收编。2.2 为什么Map注入比手写工厂省心手写策略工厂的经典代码长这样public class DiscountStrategyFactory { private static final MapString, DiscountStrategy MAP new HashMap(); static { MAP.put(vip, new VipDiscountStrategy()); MAP.put(normal, new NormalDiscountStrategy()); } public static DiscountStrategy get(String level) { return MAP.get(level); } }问题很明显新增策略要改静态代码块忘了注册就会出现线上NPE策略对象自己newSpring的AOP、事务、依赖注入这些能力全部享受不到。如果策略类里还需要注入别的服务——比如VIP策略要查用户积分、还要调风控接口——手写工厂会让你非常痛苦你必须手动组装依赖链条。Spring Bean策略把这些脏活全包了。策略是一个Spring管理的Bean它可以正常Autowired其他依赖可以参与事务可以被AOP切面拦截生命周期由容器管理。你新增一个策略类只需要关注策略本身的逻辑注册和查找完全透明。这是Spring容器天然提供的依赖注入版工厂模式比你自己写工厂类高级得多。2.3 变体需要动态获取Bean时的处理方式Map注入适合策略Key在编写时能确定的情况。如果Key本身是动态的、或者你想按某个规则匹配多个策略可以考虑ApplicationContext手动获取的变体Autowired private ApplicationContext applicationContext; public DiscountStrategy getStrategy(String beanName) { return applicationContext.getBean(beanName, DiscountStrategy.class); }这种写法更灵活但我不建议在常规业务里优先用它。原因有两个第一ApplicationContext.getBean是运行时字符串查找IDE跳转、编译期检查全部失效类型安全大打折扣第二失去构造器注入的显式声明测试时Mock策略比较麻烦。Map注入能覆盖大部分场景真遇到动态需求再额外加一个getBean方法兜底就够了。2.4 绕开两个高频坑第一个坑是Bean名冲突。MapString, DiscountStrategy的Key是Spring的BeanName默认是类名首字母小写。如果你两个策略类恰好同名不同包或者你自己用了Bean而且名字起重复了容器启动就会报错。建议给每个策略指定一个业务含义明确的Bean名像Component(vip)这样别依赖默认名。第二个坑是策略Bean的状态。Spring默认单例如果策略类内部维护了可变状态在高并发下会出问题。比如有人为了省事直接在策略里存一个上次计算的临时变量那等于给所有线程共享了一个可变缓冲区。策略类应该是无状态的或者把状态设计成方法参数传进来。如果确实需要每个策略有独立状态要把Scope改成prototype同时注意注入方式。提示判断一个策略该不该用Spring Bean管理最直接的指标是——这个策略要不要依赖其他Bean要不要被事务或切面拦截如果答案都是否并且策略逻辑也不复杂直接看后面两种方式不要为了用Spring而用Spring。3. 枚举策略让编译器帮你约束策略集合3.1 枚举内部定义抽象方法的基本姿势策略模式的一个变体是用Java枚举实现策略接口。这种方式适合策略集合固定、不会频繁扩张的场景。最典型的就是状态机、操作类型、渠道类型这类业务概念它们本身就有一组固定取值枚举天然是它们的容器。最基本的写法是枚举内部定义抽象方法每个枚举常量提供自己的实现public enum OrderStatusAction { PAY(1, 待支付) { Override public void handle(OrderContext ctx) { // 支付状态的处理逻辑 } }, SHIP(2, 待发货) { Override public void handle(OrderContext ctx) { // 发货状态的处理逻辑 } }, COMPLETE(3, 已完成) { Override public void handle(OrderContext ctx) { // 完成状态的处理逻辑 } }; private final int code; private final String desc; OrderStatusAction(int code, String desc) { this.code code; this.desc desc; } public abstract void handle(OrderContext ctx); }客户端调用时只需要OrderStatusAction.PAY.handle(ctx)编译器保证枚举常量的值只能来自定义好的那几项传错值直接编译报错。这种类型安全带来的好处是巨大的——一个“意外传错字符串”的Bug就再也发生不了。3.2 进阶枚举实现统一策略接口兼顾开放与约束枚举内部定义抽象方法有个问题策略逻辑和枚举绑得太死每个枚举常量里的代码如果多了枚举类会迅速膨胀。更优雅的做法是枚举只负责实现统一的策略接口逻辑还是按接口来组织public interface LoginStrategy { boolean login(String userName, String credential); } public enum LoginType implements LoginStrategy { PASSWORD { Override public boolean login(String userName, String credential) { return PasswordService.verify(userName, credential); } }, SMS { Override public boolean login(String userName, String credential) { return SmsService.verify(userName, credential); } }, LDAP { Override public boolean login(String userName, String credential) { return LdapService.verify(userName, credential); } } }调用方持有的是LoginStrategy接口引用枚举常量是这个接口的实现。这样做的好处是双向的接口保证了调用方不依赖具体实现枚举保证了策略集合是封闭的、可枚举的。这也是很多框架源码里常见的组合手法。3.3 枚举策略的隐藏优势除了类型安全枚举策略还有几个经常被忽略的优势序列化和比较天然支持枚举单例、可序列化比较直接可用。你从一个请求里解析出登录类型后直接用LoginType.SMS loginType判断即可不需要担心HashMap或字符串比较带来的额外开销。自带业务元数据状态码、描述信息、前后置状态都可以放在枚举字段里策略本身和业务元数据在一个类里内聚比接口实现类带常量映射的方式更直观。文档即代码所有可选策略一目了然新接手代码的人看一眼枚举常量就知道系统支持哪些策略不需要翻遍所有实现类。3.4 别让枚举策略变成新的上帝类枚举策略的边界一定要控制住。我见过有人把几十个分支逻辑全塞进一个枚举里一个枚举类写了上千行这比if-else还难维护——至少if-else还有顺序可以读枚举常量内部的匿名类一多代码评审的时候眼睛都看花。我自己的判断标准是策略数量不超过10个并且在一段时间内新增频率很低用枚举合适。如果策略集合天然就是已知的、固定的业务属性枚举是非常好的选择。但如果项目还在快速迭代、策略经常要加枚举会让你每加一个策略都改一个已有类这违反开闭原则不如直接用Spring Bean方案。另一个要特别注意的是枚举常量是静态的、全局唯一的Spring容器管不到它。如果你在枚举策略里想调用Spring管理的Service不能直接在枚举上写Autowired那没用。常见的处理办法是把依赖作为方法参数传进来或者在启动时初始化一个静态依赖持有器。更简单的是枚举方法里通过ApplicationContextHolder获取Bean。我通常不推荐在枚举里做深度的Spring依赖操作如果策略逻辑依赖大量外部服务这本身就是一种信号策略已经不适合用枚举表达了。4. Lambda策略用函数式思维给策略模式减重4.1 函数式接口就是天然的策略接口Java 8引入Lambda表达式之后策略模式的实现又轻了一档。如果一个策略只有一个方法那它本质上就是一个函数式接口。你不需要为了一个策略写一个实现类也不需要为它在枚举里写匿名内部类直接用Lambda表达式就完事了。函数式接口在JDK里有现成的FunctionT,R、PredicateT、ConsumerT、SupplierT但你完全可以按业务语义自定义一个带名字的接口可读性更好FunctionalInterface public interface PriceCalculator { BigDecimal calculate(BigDecimal originalPrice); } public class PriceService { private final PriceCalculator normalPrice price - price; private final PriceCalculator vipPrice price - price.multiply(new BigDecimal(0.85)); private final PriceCalculator seckillPrice price - price.subtract(new BigDecimal(50)); }以往你得建三个实现类现在三行Lambda全搞定。如果策略只有一行算法非要建一个类那纯属浪费。4.2 用枚举装Lambda轻量策略的最优解枚举和Lambda的组合是我个人非常偏爱的一种轻量策略实现方式。枚举提供类型安全的策略集合Lambda提供紧凑的实现。还是折扣计算的例子public enum DiscountType implements DiscountStrategy { NONE(amount - amount, 无折扣), VIP(amount - amount.multiply(new BigDecimal(0.8)), VIP八折), COUPON(amount - amount.subtract(new BigDecimal(30)), 满减30); private final FunctionBigDecimal, BigDecimal discountFunc; private final String desc; DiscountType(FunctionBigDecimal, BigDecimal discountFunc, String desc) { this.discountFunc discountFunc; this.desc desc; } Override public BigDecimal calculate(BigDecimal amount) { return discountFunc.apply(amount); } }策略逻辑短小专注枚举承载策略元数据客户端一行调用DiscountType.VIP.calculate(price)。这种写法在促销规则、审批流处理、导入导出解析这类场景里非常实用代码量比传统策略类少了一半以上。不过要清醒Lambda装枚举的前提是逻辑简单。如果Lambda体里要写七八行还要调用好几个Service那说明策略复杂度已经超过了表达式的承载能力还是老老实实抽成方法或策略类吧。4.3 在Spring中如何结合Lambda与BeanLambda策略和Spring也不冲突。你可以把Lambda包装在一个Bean里提供让策略逻辑既能用Spring的依赖注入又能享受Lambda的简洁。一个常见的做法是使用Bean方法返回函数式接口Configuration public class RewardStrategyConfig { Bean public FunctionOrder, RewardResult newUserReward() { return order - { if (order.getAmount().compareTo(new BigDecimal(100)) 0) { return new RewardResult(new_user_coupon, new BigDecimal(20)); } return new RewardResult(none, BigDecimal.ZERO); }; } Bean public FunctionOrder, RewardResult inviteReward() { return order - new RewardResult(invite_coupon, new BigDecimal(10)); } }然后在Service里注入一个MapString, FunctionOrder, RewardResult按规则名称取用。这和第一节的Spring Bean策略用法完全一致区别只是Bean的类型从策略接口类变成了函数式接口。如果你的策略本身就只关心输入输出转换这件事用Function、Predicate这类JDK内置函数式接口可以让代码更直白。4.4 Lambda策略的适用边界与两个反例Lambda策略不是万能的我总结两个反例提醒大家注意第一个反例是把复杂策略硬写成Lambda。策略涉及多个方法、内部有状态、需要复用公共逻辑时Lambda会让代码变成一团没法调试的表达式。这个时候你需要的还是传统的策略类方法拆开状态收敛逻辑清晰可测试。第二个反例是滥用内置函数式接口导致语义模糊。比如一个接收订单返回折扣价的策略你用FunctionOrder, Price表达阅读者要花时间理解输入输出。如果定义一个OrderDiscountCalculator接口方法名写清楚calculateDiscount(Order): Price可读性立刻提升。在团队协作里代码可读性跟逻辑正确同样重要不要为了少写几行类定义牺牲语义表达能力。5. 三种方式的取舍逻辑与组合实践5.1 一张表讲清选型依据很多人在策略模式上面卡住不是不懂原理而是不知道选哪种实现落地。我经常被问到这三种到底用哪个下面这张表是我在实际项目里反复验证后的结论维度Spring Bean策略枚举策略Lambda策略策略数量增长扩展性最好新增类即可符合开闭原则策略集合固定时很好频繁扩展需要改已有枚举适合少量轻逻辑多了会难读类型安全弱。Key是字符串运行时才知道对错最强。编译器强制约束较强。接口签名约束输入输出类型Spring依赖注入最顺畅AOP、事务、Bean生命周期全支持不友好枚举静态实例无法自动注入可借助Bean配置注入代码量接口多个实现类代码量较大中等一个枚举包含策略和元数据最少表达式即策略复用性最好策略类可单独测试、复用好枚举本身就是单例较好函数式接口可组合复用适合场景支付渠道、登录方式、消息推送等插件化需求状态机、流程节点、固定操作类型简单转换、筛选、小规则计算5.2 我的真实选择顺序在一般业务代码里我的选择顺序是能枚举就枚举枚举装不下再考虑Spring BeanSpring Bean嫌重就用Lambda做局部优化。什么意思呢当一个策略集合是业务概念本身的固定属性——比如订单状态流转、审批操作类型、登录方式——我用枚举策略。它自带类型安全、元数据、序列化能力写起来还省代码。当策略集合未来大概率要扩展、且每个策略逻辑较重、需要依赖其他Spring服务时我用Spring Bean策略Map注入一件收编扩展一个类搞定。当策略只是某个算法细节、只有一行几行代码时我用Lambda把策略就地解决不为琐碎逻辑建类。有一类需求会同时用到两种甚至三种方式。比如我现在维护的一个会员权益系统权益类型是有限的、固定的适合用枚举定义权益类型和元数据但每种权益的具体发放逻辑差异很大有的要调外部卡券平台有的要发内部积分这时候枚举内部的Lambda表达式就撑不住了需要把具体发放逻辑下沉到Spring管理的内部类或Service里枚举只做分发和兜底判断。这样组合起来既有枚举的约束力又有Spring的扩展力。5.3 组合实践中我踩过的坑坑一枚举策略里塞Spring依赖项目启动报NullPointerException。原因就是前面提的枚举是静态常量Spring的依赖注入发生在容器启动时而枚举在类加载时就初始化了注入根本来不及。解决方案是不要在枚举里持有注入的Service改为方法参数传服务或者通过静态ApplicationContextHolder取。坑二Map注入时多个策略Bean被Spring容器收集到了但Key和自己的预期不一样。我遇到过用DefaultDiscountStrategy命名的类默认BeanName是defaultDiscountStrategy业务里却用default去取取出来是null排查了半天才发现是命名对不上。解决方法是显式指定BeanName并且最好在接口上提供一个strategyCode()方法启动时用ApplicationRunner做一次映射校验发现错误尽早暴露。坑三策略太多了Map注入变成垃圾回收的重灾区。如果你有上百个策略类并且每个策略都注入了一大堆Service应用启动时间会明显变长内存占用也变大。这时候需要做延迟实例化或按需加载处理而不是让所有策略Bean都饿汉式初始化。不过一般业务系统到不了这个量级遇到再说。6. 一个支付渠道场景的完整重构演示6.1 重构前一团乱麻的分支方法光讲理论不落地都白搭最后我用一个大家都见过的支付渠道选择场景把三种方式串起来。假设你现在负责一个电商系统支付方法长这样public PayResult pay(Order order, String channel) { if (alipay.equals(channel)) { // 支付宝签名、调API、处理回调... return payByAlipay(order); } else if (wechat.equals(channel)) { // 微信签名、调API、处理回调... return payByWechat(order); } else if (unionpay.equals(channel)) { // 银联处理... return payByUnionpay(order); } else { throw new IllegalArgumentException(unknown channel); } }每次加一个支付渠道都要打开这个老方法加一个else if。如果每个渠道的处理逻辑是15行三渠道就是45行渠道多了这个方法的可读性会跌到谷底。6.2 第一步用Spring Bean策略重写定义一个支付策略接口public interface PaymentStrategy { boolean supports(String channel); PayResult pay(Order order); }支付宝实现类Component public class AlipayStrategy implements PaymentStrategy { Autowired private AlipayClient alipayClient; Override public boolean supports(String channel) { return alipay.equals(channel); } Override public PayResult pay(Order order) { // 支付宝特有逻辑 return alipayClient.pay(order); } }微信实现类类似注入微信的SDK Client。然后在支付Service里用Map注入改写法Service public class PaymentService { private final MapString, PaymentStrategy strategyMap; public PaymentService(MapString, PaymentStrategy strategyMap) { this.strategyMap strategyMap; } public PayResult pay(Order order, String channel) { PaymentStrategy strategy strategyMap.get(channel); if (strategy null) { throw new IllegalArgumentException(unsupported channel: channel); } return strategy.pay(order); } }到这里主要的分支判断已经没了新的支付渠道只需要新增一个实现类不用动PaymentService。Map的Key就是BeanName我用Component(alipay)、Component(wechat)这类显式名称保证Key和渠道编码一致。6.3 第二步用枚举加Lambda优化支付方式类型本身支付渠道的枚举还可以承载更多业务元数据比如渠道名称、logo地址、手续费比例。这部分用枚举装Lambda的方式很合适public enum PayChannel { ALIPAY(alipay, 支付宝, 010, BigDecimal.ONE), WECHAT(wechat, 微信支付, 020, BigDecimal.ZERO), UNIONPAY(unionpay, 银联, 030, new BigDecimal(0.5)); private final String code; private final String displayName; private final String bankNo; private final BigDecimal feeRate; PayChannel(String code, String displayName, String bankNo, BigDecimal feeRate) { this.code code; this.displayName displayName; this.bankNo bankNo; this.feeRate feeRate; } }这样渠道码、展示名、费率和渠道策略通过枚举统一管理前端展示、后端结算计算都可以直接复用这个枚举。策略本身还是Spring Bean那套因为支付逻辑依赖外部SDK和回调处理这类逻辑太重不适合塞进枚举常量里。6.4 第三步渠道内子规则的Lambda化处理进一步观察发现微信支付下还有两种子场景APP支付和扫码支付。它们的流程大致相同只是调用方式不同。如果为这两个子场景各自建策略类就有点杀鸡用牛刀了我直接在微信策略类内部用Lambda处理这个子分发Component(wechat) public class WechatPayStrategy implements PaymentStrategy { Autowired private WechatClient wechatClient; private final MapString, FunctionOrder, WechatPayRequest sceneMapper Map.of( APP, order - WechatPayRequest.buildApp(order), NATIVE, order - WechatPayRequest.buildNative(order) ); Override public PayResult pay(Order order) { FunctionOrder, WechatPayRequest requestBuilder sceneMapper.get(order.getPayScene()); WechatPayRequest request requestBuilder.apply(order); return wechatClient.pay(request); } }整体下来支付链路的代码变成渠道层用Spring Bean策略做插件化扩展渠道类型用枚举承载元数据渠道内部的子场景用Lambda做轻量化分发。三种方式各司其职没有一种是万能的但组合起来非常顺手。6.5 重构后的收益与后续扩展方向重构完这个场景最直观的感受有几个首先pay(Order, String)这个方法从二十几行的if-else缩成了四五行的查表分发一眼就知道整体逻辑其次新增一个支付渠道新写一个类支付Service完全不用动测试也只用补这个新渠道的用例回归成本低再者渠道枚举给前端、结算、报表提供了统一的数据来源不会出现前端一套渠道字典、后端一套渠道映射的割裂情况。如果后续还要扩展比如加一个多渠道组合支付的需求可以在Spring Bean策略的基础上再加一个聚合策略类内部编排多个渠道的策略调用顺序如果要加支付超时自动关单用Lambda挂一个定时任务策略即可渠道的可用性配置比如某个渠道临时维护中也可以在枚举里加一个available字段动态控制。这些扩展方向上策略模式配合Spring的包容性都留好了口子。我个人的经验是策略模式不贵在用了哪种实现贵在分清每种实现的性子。Spring Bean策略是重型武器适合大逻辑、可扩展、依赖多的场景枚举策略是中型装备适合固定分支、类型安全优先、自带元数据的场景Lambda策略是轻量匕首适合几行代码的算法替换能不动类结构就不动。这三样在项目里往往并存分场景使用才能让代码既有战斗力又不笨重。