1. 为什么说策略模式是Java项目里最被低估的设计模式
先聊个很现实的场景:你负责维护一个订单系统,业务方今天说“结算方式要支持支付宝”,明天说“再加个云闪付”,后天可能又冒出来一个“数字人民币”。第一版你可能写了一个switch,对着payType逐个判断,瞧着挺清爽。等分支增加到六七个的时候,这段代码已经变成谁碰谁炸的雷区。改一个分支,怕影响另外几个;加一个分支,恨不得把整个方法重新review一遍。
这时候策略模式的价值就体现出来了。它不复杂,甚至可以用一句话讲明白:把每个算法或行为封装成独立的类,让它们在运行时可以互相替换。但真正把它用好,让它从“面试八股文”变成“项目里的趁手工具”,你需要理解的不只是那三个角色——Context、Strategy、ConcreteStrategy——更要理解为什么这样拆,什么时候该拆,以及拆完以后怎么避免过度设计。
这篇文章就是写给两类人看的:一类是还在背面试题、没有完整项目经验的初学者,另一类是已经写了几年Java但一直在if-else里挣扎的同学。我会尽量用直白的话把原理讲透,再用一个能直接落地的案例把整个过程串起来,最后把我在实际项目里踩过的坑和总结出来的经验一并倒出来。
2. 策略模式的核心思想拆解:它到底在解决什么问题
策略模式本质上是在践行面向对象设计里的两个基本原则:封装变化和面向接口编程,而非面向实现编程。很多教程一上来就画UML图,初学者看完图更懵了。我换个说法。
2.1 把“做什么”和“怎么做”拆开
策略模式要解决的核心矛盾是:行为的使用者和行为的实现者不该死死绑在一起。举个例子,你去餐厅点菜,菜单是“行为的使用者”,后厨是“行为的实现者”。你只需要告诉服务员“来一份鱼香肉丝”,不需要知道是哪个厨师炒的、用什么火候、放多少盐。哪天换了个厨师,菜谱变了,你点菜的方式完全不受影响。策略模式就是把“点菜”和“做菜”之间的耦合关系切断。
放到代码里,这个“菜谱”就是策略接口,每个“厨师”就是具体的策略实现。调用方只依赖策略接口,不依赖任何具体实现。这样带来的一个直接好处是:新增一种策略时,现有的代码一行都不用改。这就是设计模式里常说的“开闭原则”——对扩展开放,对修改关闭。
2.2 策略模式的三要素
虽然很多资料喜欢画复杂的UML图,但策略模式的结构其实就三个角色:
- 策略接口(Strategy):定义了一个算法的规范,比如“计算价格”这个方法长什么样。
- 具体策略(ConcreteStrategy):实现策略接口的各个类,每个类里是一种具体的算法或行为。
- 上下文(Context):持有策略接口的引用,负责调用策略。它本身不关心策略是怎么实现的,只负责把请求转发给当前持有的策略对象。
很多人学到这里会疑惑:Context到底该做什么?其实Context的角色很轻,它就像是“点菜的服务员”,你告诉他你要什么策略,他就把需求转达给对应策略。策略的具体逻辑、细节,全部在具体策略类里完成。
2.3 与“if-else”“switch”的本质区别
这里要澄清一个非常普遍的误解:很多人以为策略模式就是“用多态替代switch”,于是写了一个策略接口,然后仍然用if-else去判断该new哪个实现类。这就有点本末倒置了。
if-else本身没有错,问题在于选择逻辑和业务逻辑混在一起。策略模式的价值在于把业务逻辑拆散到各自的类里,让每个类足够内聚,而“选择哪个策略”这个动作可以单独处理。后文我会讲到,选择逻辑可以用工厂、枚举、Map或者Spring的依赖注入来管理。所以策略模式的目标不是消灭switch,而是把代码的变化点集中管理起来。
3. 从零写一个完整案例:支付方式的策略模式实现
理论讲再多,不如一个能跑的demo。我挑了一个最经典也最贴近实际业务的场景:支付方式选择。这个案例能自然展示策略模式的完整过程,从最原始的if-else写法,一步一步重构成策略模式,你才能直观感受到区别。
3.1 第一版:土味十足的if-else实现
先看大多数项目的初始状态。假设有一个OrderService,根据支付方式不同,走不同渠道扣款,最后统一更新订单状态。
public class OrderService { public void pay(String payType, BigDecimal amount) { if ("ALIPAY".equals(payType)) { // 调用支付宝SDK System.out.println("支付宝支付:" + amount + "元"); // 省略一堆业务逻辑... } else if ("WECHAT".equals(payType)) { // 调用微信SDK System.out.println("微信支付:" + amount + "元"); // 省略一堆业务逻辑... } else if ("UNIONPAY".equals(payType)) { // 调用银联SDK System.out.println("银联支付:" + amount + "元"); // 省略一堆业务逻辑... } else { throw new IllegalArgumentException("不支持的支付方式: " + payType); } // 支付完成后的公共逻辑,比如更新订单状态 System.out.println("支付成功,订单状态已更新"); } }这段代码的问题,有经验的同学一眼就能看出来:每当新增一个支付渠道,就要往这个pay方法里再塞一个else if。时间一长,这个方法的长度、复杂度、以及出错概率都会飙升。更要命的是,所有支付渠道特有的参数校验、日志格式、异常处理都堆在一起,想单独看某个支付渠道的逻辑,得在一大坨代码里来回找。
3.2 第二版:策略模式的标准重构
现在我用策略模式来重构这个支付逻辑。重构分三步走:定义策略接口、实现具体策略、在上下文里调用。
第一步,定义策略接口。这个接口就是所有支付渠道的规范,“入参是什么、返回什么”都定在这里。
public interface PaymentStrategy { void pay(BigDecimal amount); }第二步,为每个支付渠道创建一个具体策略类。
public class AlipayStrategy implements PaymentStrategy { @Override public void pay(BigDecimal amount) { // 支付宝特有的参数校验、签名逻辑等 System.out.println("支付宝支付:" + amount + "元"); } } public class WechatPayStrategy implements PaymentStrategy { @Override public void pay(BigDecimal amount) { // 微信特有的逻辑 System.out.println("微信支付:" + amount + "元"); } } public class UnionPayStrategy implements PaymentStrategy { @Override public void pay(BigDecimal amount) { // 银联特有的逻辑 System.out.println("银联支付:" + amount + "元"); } }第三步,改造调用方,让OrderService持有策略接口。
public class OrderService { private PaymentStrategy paymentStrategy; public OrderService(PaymentStrategy paymentStrategy) { this.paymentStrategy = paymentStrategy; } public void pay(BigDecimal amount) { paymentStrategy.pay(amount); // 支付完成后的公共逻辑 System.out.println("支付成功,订单状态已更新"); } }这样的好处是显而易见的:OrderService再也不用关心支付渠道的细节了,它只负责调用策略,然后执行公共逻辑。新增一个支付渠道,只需要加一个实现类,OrderService一行代码都不用动。
3.3 第三版:用工厂模式解决“策略怎么选”的问题
重构到这里,细心的读者会发现一个问题:OrderService确实变干净了,但调用方在构造OrderService的时候,还是得自己选择具体的策略,比如new OrderService(new AlipayStrategy())。如果这个选择逻辑散落在各个地方,那依然有大量的new和分支判断。
这个问题的标准解法是引入一个策略工厂,把“根据什么条件选择哪个策略”这件事集中管理起来。
public class PaymentStrategyFactory { private static final Map<String, PaymentStrategy> STRATEGY_MAP = new HashMap<>(); static { STRATEGY_MAP.put("ALIPAY", new AlipayStrategy()); STRATEGY_MAP.put("WECHAT", new WechatPayStrategy()); STRATEGY_MAP.put("UNIONPAY", new UnionPayStrategy()); } public static PaymentStrategy getStrategy(String payType) { PaymentStrategy strategy = STRATEGY_MAP.get(payType); if (strategy == null) { throw new IllegalArgumentException("不支持的支付方式: " + payType); } return strategy; } }这样,客户端代码就从“写一堆if-else”变成了“交给工厂去拿一个策略对象”:
public class PaymentDemo { public static void main(String[] args) { String payType = "ALIPAY"; PaymentStrategy strategy = PaymentStrategyFactory.getStrategy(payType); OrderService orderService = new OrderService(strategy); orderService.pay(new BigDecimal("99.90")); } }工厂的引入把“策略集合”的管理中心化了,新增策略时只需要往STRATEGY_MAP里加一行,或者动态注册。这种方式在策略数量不多、类型相对固定时非常好用,代码结构一目了然。
4. 策略模式在Java 8之后的现代化写法
很多老教程写的策略模式还是传统的接口+实现类,但Java 8带来了Lambda表达式和方法引用,让策略模式的表达大大简化。如果你维护的是JDK 8+的项目,完全可以利用这些新特性,让代码更精简。
4.1 用Lambda替代单方法接口的实现类
策略接口通常是只有一个抽象方法的接口,这恰好是函数式接口的定义。比如上面那个PaymentStrategy接口,只有pay这一个方法,所以它完全可以被当作@FunctionalInterface使用。
@FunctionalInterface public interface PaymentStrategy { void pay(BigDecimal amount); }这样一来,策略实现类不一定要新建一个类文件了,直接用Lambda表达式就行:
PaymentStrategy alipayStrategy = amount -> System.out.println("支付宝支付:" + amount + "元");4.2 用Map + Lambda彻底替代策略工厂
Lambda的威力在配合工厂使用时体现得最明显。传统的static块逐个注册策略的方式,可以简化成一行行的Map赋值:
public class PaymentStrategyFactory { private static final Map<String, PaymentStrategy> STRATEGY_MAP = new HashMap<>(); static { STRATEGY_MAP.put("ALIPAY", amount -> System.out.println("支付宝支付:" + amount + "元")); STRATEGY_MAP.put("WECHAT", amount -> System.out.println("微信支付:" + amount + "元")); STRATEGY_MAP.put("UNIONPAY", amount -> System.out.println("银联支付:" + amount + "元")); } public static PaymentStrategy getStrategy(String payType) { PaymentStrategy strategy = STRATEGY_MAP.get(payType); if (strategy == null) { throw new IllegalArgumentException("不支持的支付方式: " + payType); } return strategy; } }如果策略逻辑复杂,Lambda表达式里写一大堆代码看着也会难受。我的建议是:逻辑简单的用Lambda,逻辑复杂的仍然写成独立的类。不要为了炫技把代码全部塞进Lambda里,最后变成一行几百个字符的“代码面条”,维护起来反而更痛苦。
4.3 结合枚举实现更优雅的“策略注册”
还有一种更贴近业务场景的玩法,是把策略和枚举结合起来。每个枚举项本身就是一种策略,同时在枚举里实现策略接口,这样“选择”和“定义”都在同一个地方,内聚性非常好。
public enum PaymentStrategy implements PaymentStrategy { ALIPAY { @Override public void pay(BigDecimal amount) { System.out.println("支付宝支付:" + amount + "元"); } }, WECHAT { @Override public void pay(BigDecimal amount) { System.out.println("微信支付:" + amount + "元"); } }, UNIONPAY { @Override public void pay(BigDecimal amount) { System.out.println("银联支付:" + amount + "元"); } }; }使用的时候通过枚举常量直接获取策略:
PaymentStrategy strategy = PaymentStrategy.valueOf("ALIPAY"); strategy.pay(new BigDecimal("20.00"));这种方式在策略类型非常稳定、策略数量有限的情况下非常好用。比如订单状态的流转、审批流程节点这些固定集合的场景,用枚举实现策略模式比用Map维护更安全、更直观。
5. 策略模式之外:三种替代选型你得心里有数
经常有人问我:“策略模式虽然好,但也不是所有场景都适合。”这句话是对的。设计模式不是银弹,策略模式也有它的适用边界。下面这三种做法,在实际项目中都是策略模式的常见替代方案,各有各的优劣势。
5.1 策略模式 vs. 简单if-else
如果只有两三个分支,且每个分支里的代码量很少,那直接用if-else没有任何问题。强行上策略模式,反而会创造一个接口加两三个实现类,代码量和复杂度都上去了。过度设计是初学者最容易犯的毛病。我的判断标准很简单:分支数目超过三个,或者每个分支里的逻辑行数超过十行,我才会认真考虑策略模式。如果只是二选一的简单判断,写个if-else干净利落,没必要为了模式而模式。
5.2 策略模式 vs. 状态模式
状态模式和策略模式的类图长得很像,很多人学完就混了。核心区别在于:策略模式是客户端主动选择算法,状态模式是对象内部根据状态变化自动切换行为。举个例子,订单的状态有“待支付”“已支付”“已发货”“已完成”,每个状态下“允许执行的操作”不同,这是状态模式;支付方式有支付宝、微信、银联,用户主动选择用哪种方式付款,这是策略模式。一个是被动切换,一个是主动选择,从语义上就能区分开来。
5.3 策略模式 vs. 责任链模式
还有一种容易混淆的情况,比如“不同类型的消息分别用不同处理器处理”“不同级别的日志分别走不同渠道”。有人会想用策略模式,有人会想用责任链。这两种模式的差别在于:策略模式是选择一个处理器来处理,责任链模式是让多个处理器依次尝试,直到有人能处理。如果业务上只能选一个策略执行,用策略模式;如果多个处理器都有可能参与,且需要逐个传递,那就该用责任链模式。
6. 真实项目里的Spring Boot实践:策略模式+依赖注入的黄金组合
上一节讲的都是纯Java的写法。回到实际开发,绝大多数Java项目都在用Spring Boot,这种情况下策略模式的玩法还能再进化一步。借助Spring的依赖注入,连那个维护策略的Map都可以免了,框架直接帮你把所有的策略实现类注入进来,真正做到“开箱即用”。
6.1 把所有策略实现类注入到一个Map
在Spring里,你可以把PaymentStrategy接口的所有实现类都注入到一个Map<String, PaymentStrategy>中,Map的key就是Bean的名字。注意,这个Map的注入是Spring容器自动完成的,不需要你手动new和put。
@Service public class OrderService { private final Map<String, PaymentStrategy> paymentStrategyMap; public OrderService(Map<String, PaymentStrategy> paymentStrategyMap) { this.paymentStrategyMap = paymentStrategyMap; } public void pay(String payType, BigDecimal amount) { PaymentStrategy strategy = paymentStrategyMap.get(payType); if (strategy == null) { throw new IllegalArgumentException("不支持的支付方式: " + payType); } strategy.pay(amount); // 公共逻辑 System.out.println("支付成功,订单状态已更新"); } }每个具体的支付策略类用@Component标注,默认的Bean名称就是类名首字母小写。比如:
@Component public class AlipayStrategy implements PaymentStrategy { @Override public void pay(BigDecimal amount) { System.out.println("支付宝支付:" + amount + "元"); } }这样,Spring启动时会把AlipayStrategy、WechatPayStrategy等所有实现类当作Bean注册进容器,然后按类型注入到orderService的paymentStrategyMap里,Map的key就是"alipayStrategy"、"wechatPayStrategy"这些Bean名称。调用方把支付类型传进来,直接从Map里取对应的策略就好了。
6.2 给Bean起别名,保持支付类型和编码风格统一
上面有个小细节需要注意:默认的Bean名称是“alipayStrategy”这种驼峰格式,如果业务传入的支付类型是“ALIPAY”这种大写枚举风格,两者就对不上。这种情况可以给Bean显式命名:
@Component("ALIPAY") public class AlipayStrategy implements PaymentStrategy { // ... } @Component("WECHAT") public class WechatPayStrategy implements PaymentStrategy { // ... }这样传入“ALIPAY”时,就可以直接命中对应的Bean。这个方法在业务上很常见,尤其是支付类型、业务类型这些约定俗成的编码,直接作为Bean名称来注册,逻辑简单又直观。
6.3 处理“未知策略”的兜底方案
策略模式在真实项目里最容易翻车的就是“查不到策略”。比如前端传了一个不存在的支付方式,或者服务之间的调用方传了一个错误编码,这种情况如果直接抛IllegalArgumentException,在高并发的生产环境里可能就是一堆异常日志。更友好的做法是定义一个兜底策略,比如DefaultPaymentStrategy,或者返回一个标准的失败结果。具体用哪种,取决于你的业务是面向C端用户还是B端内部调用。面向C端,一定要兜底,别让用户看到500页面;面向B端内部调用,则要抛出明确异常,方便排查问题。
我用Spring的写法再延伸一下,兜底策略可以单独放一个Bean,放在Map里最后一个,或者实现ApplicationRunner在启动时注册到Map的尾部。核心逻辑是:Map里查不到时不要立刻抛异常,先尝试兜底策略,兜底策略里可以做告警、降级,再决定返回什么结果。
6.4 Spring Boot实战中的避坑建议
- 不要把策略实现类都塞进一个包然后靠反射扫描。用Spring的依赖注入已经足够,反射反而会让代码晦涩难懂,性能也更差。
- 注意Bean的加载顺序。如果有多个策略实现类互相依赖,要小心循环依赖问题。
Map<String, Strategy>注入时,如果同一个类型有多个Bean,必须确保Map的泛型是正确的,Spring才能自动按类型注入。泛型不对是新手容易踩的坑。
7. 从面试到实战:策略模式常见问题和高频考法
说完了代码层面的东西,再来看看面试环节。策略模式在Java面试里几乎是必考题,尤其是那些做后端开发的岗位。面试官问策略模式,通常不是让你背定义,而是看你能不能结合实际场景把它讲清楚。下面几个高频问题,我逐个拆一遍。
7.1 策略模式和状态模式的区别
这是最容易考到的一道题,也是区分初学者和老开发的关键点。前面我简单讲过,这里再归纳成一句话:策略模式是“你选算法”,状态模式是“状态决定行为”。为了更形象,可以举一个具体的例子:文本编辑器里的“查找替换”功能,查找算法可以是“从前往后找”“从后往前找”“正则匹配”,这是策略模式;而订单状态从“已支付”变成“已发货”后,能执行的操作从“申请退款”变成“确认收货”,这是状态模式。一个由用户主动选择,一个由当前状态自动切换,性质完全不同。
7.2 策略模式在JDK中有哪些经典应用
这个问题能检验你是不是真的读过源码。比较常见的回答有:
java.util.Comparator:排序算法策略,Collections.sort的时候传入不同的Comparator,就是策略模式的体现。java.util.concurrent.ThreadPoolExecutor的RejectedExecutionHandler:线程池满了以后的拒绝策略,AbortPolicy、CallerRunsPolicy、DiscardPolicy这些都是不同的策略实现。javax.servlet.http.HttpServlet的service方法在分发请求时,本质上也是策略模式的体现,doGet、doPost是不同策略。
能说出这几个例子,面试官基本能确认你不是只会背概念。
7.3 策略模式的缺点
这个问题很多人答不上来,或者只会说一句“类变多了”。其实策略模式的主要缺点有三个:
- 类爆炸:每新增一种策略就要新建一个类,策略很多时类数量增长很快。
- 调用方必须了解各个策略的区别:虽然策略模式解耦了“怎么实现”,但调用方还得知道“每个策略分别有什么效果”,否则不知道该选哪个。这也是为什么常常需要配合工厂模式的另一个原因——把“策略选择”的复杂度也封装起来。
- 无法限制策略的使用场景:某些策略只在特定条件下有效,如果不加约束,调用方可能在错误的场景下选用了不合适的策略。低级做法是看注释,高级做法是在策略类里加校验逻辑,或者在工厂里做条件判断。
7.4 策略模式能带来哪些可测试性提升
这一条在实战里特别重要,但面试中很少被问到。策略模式把每个算法隔离成独立的类,这意味着你可以对每个策略单独做单元测试,不需要模拟一堆外部依赖。比如支付策略,你可以为每个渠道写一个测试类,只验证该渠道的签名逻辑、请求参数、异常处理,测试代码和被测试的策略类一一对应,非常清晰。
8. 策略模式的实战经验总结:哪些坑我替你踩过了
最后分享几条我在实际项目中应用策略模式总结出来的经验。这些内容在教科书里通常找不到,但真正决定你能不能用好这个模式的,恰恰是这些细节。
8.1 策略类无状态优先
设计策略类的时候,尽量让它保持无状态,也就是不要持有会变化的数据字段。策略类应该像工具类一样,接收参数、处理逻辑、返回结果。如果一个策略类内部持有可变的成员变量,在多线程环境下就会出问题——多个线程共享同一个策略实例时,变量的读写会产生并发冲突。
解决方案有两种:一是所有参数都通过方法参数传递,彻底保持无状态;二是有状态的话,用ThreadLocal或者把策略类声明为Prototype作用域。但说实话,最好的办法还是设计成无状态的,不仅线程安全,还便于复用。
8.2 策略的命名要反映业务语义,而不是实现细节
这一点看似小事,但在维护阶段影响巨大。很多人的策略类命名是AlipayStrategyImpl、WechatStrategyIml这种,看起来没问题,但到了三种支付方式变成五种、类数量膨胀时,你根本不知道WechatNewStrategyImpl和WechatOldStrategyImpl之间的区别。我建议的命名风格是在接口名后加上清晰的业务场景标识,比如AlipayQrCodeStrategy、AlipayH5Strategy,让人一眼就能看出这是给哪个场景用的策略。
8.3 策略选择逻辑必须集中收敛
这是我在Review代码时最常发现的问题。很多团队引入了策略模式,但“如何选择策略”的逻辑散落在各个Controller、Service里,到处都在new策略类,到处都在写if-else判断。这等于把switch从一处拆到了多处,问题不但没有缓解,反而更难维护了。记住,策略模式的收益很大程度上取决于“策略选择”是否收敛。要么集中在工厂里,要么用Spring的Map注入,要么用枚举管理,选一种方式作为团队约定,不要每个开发各写各的。
8.4 注意策略与业务校验的边界
有些策略模式翻车案例,问题不在模式本身,而在于把太多业务校验塞进了策略类里。比如支付宝策略里既做了金额校验,又做了风控校验,还做了日志埋点。这样的结果是策略类内部又臭又长,失去了“算法可替换”的轻盈感。我个人经验是:策略类应该只关注“如何使用当前策略执行核心逻辑”,通用校验放在调用方前置处理,策略特有的校验放在策略类内部。不然你前后端的参数校验逻辑分布在各个策略类里,排查问题会想哭。
8.5 一个非支付场景的扩展思路:价格计算器
很多教程只拿支付举例,容易让人以为策略模式只能用在“渠道类”场景。实际上,只要业务里有一组算法可以互相替换,策略模式都能派上用场。我第二个常用的场景是价格计算器:普通会员、黄金会员、铂金会员的折扣算法不同,促销活动期间的满减规则不同,再加上不同品类的运费策略,这种排列组合用if-else写会极其痛苦。用策略模式后,每种计价规则是一个策略,再通过一个PriceCalculator组合多个策略,代码结构立刻清晰了。
比如接口定义成:
public interface PriceStrategy { BigDecimal calculate(BigDecimal originalPrice, UserContext userContext); }会员折扣、满减、运费分别实现各自策略,再用组合器按顺序执行。这种方式可扩展性极强,新增一种活动规则时,不会碰其他任何代码。
9. 最后再分享一点学习心得
策略模式是我个人认为入门门槛最低、收益最明显的一个设计模式。它不像工厂模式那样需要绕几个弯才能理解,也不像代理模式那样涉及字节码等底层机制。它的核心思想就藏在一句大白话里:把变的部分和不变的部分拆开,让变的部分可以独立改变。
如果你刚接触设计模式,我的建议是先从策略模式入手,找一个实际的业务场景,比如把一段自己写的臃肿的if-else改成策略模式,体会一下前后的差别。如果你已经会用策略模式,那接下来值得深入研究的是怎么和Spring、和函数式编程结合,让表达更简洁。学完以后你会发现,真正的设计模式不是背出来的,是写代码写到一定程度后自然生长出来的——策略模式就是个很好的起点。