策略模式实战指南:从if-else重构到优雅的算法替换
从if-else的泥潭里爬出来的那一刻你就再也回不去了。策略模式就是这么个东西它不教你任何新的语法不依赖任何框架特性纯粹是把你脑子里的分支逻辑重新组织了一遍。很多朋友在学Java设计模式的时候觉得策略模式特别简单——定义一个接口写几个实现类上下文里持有一个接口引用OK完事。但真到了项目里面对一坨促销规则、支付渠道、消息推送的diff逻辑还是不知道该从哪里下手。我写这篇就是把我自己从初学、到看源码、再到重构老旧系统时用策略模式踩过的坑和摸出来的门道一次讲透。如果你是准备面试的实习生、正在写业务系统的CRUD程序员或者是接手了遗留代码想动手重构的人这篇都很适合你。我尽量不堆概念直接用代码说话每一步都告诉你为什么这么做做完之后能避开什么坑。1. 策略模式到底在解决什么问题1.1 生活化类比打车软件的计价规则先别急着看定义。你打开打车软件同一段路程不同时间、不同车型价格完全不一样——高峰期有高峰溢价下雨天有动态调价普通时段就是标准计费。对用户来说你只需要知道最后的价格对软件来说计价规则却有好几套。如果这些规则全部写在同一个计价方法里用if-else串起来那一旦新增一个活动、调整一次折扣你就要去动那个巨大的方法改错一个分支就可能把所有用户的账单算崩。策略模式在这个场景里的做法是把每一套计价规则单独封装成一个计价器。普通时段的计价器、高峰时段的计价器、雨天的计价器各自实现同一个计价接口。打车软件在运行时只需要选择对应的计价器然后调用它的计算价格方法。用户不关心内部用了哪套规则计价器之间也不互相干扰。这就是策略模式的全部本质。1.2 策略模式解决的四类痛点第一类痛点if-else或者switch-case无限膨胀。每增加一种业务规则就在方法里多一个分支方法越来越长复杂度越来越高代码的可读性和可维护性直线下降。第二类痛点算法代码无法复用。同样的计价逻辑在这段代码里写一遍在订单模块里又写一遍换了个项目组成员可能第三份就变样了。策略模式把算法独立成类天然就是一个可以复用的组件。第三类痛点调用方被迫了解所有细节。如果你的方法里if-else判断一堆调用方只能把全部分支都写完等于把业务规则的判断逻辑耦合到了每一处调用点。策略模式把选择逻辑和具体实现拆开之后调用方只需要面向接口编程完全不关心谁在背后执行。第四类痛点违反开闭原则。新增一种算法就得改原有代码而且改的地方可能不止一处——每个调用点都得跟着加分支。策略模式把新增算法变成新增一个类不用动任何原有代码真正实现对扩展开放、对修改封闭。2. 三大核心角色与初始实现2.1 三个角色的职责边界策略模式里有三个固定角色这三个角色的边界划分是理解整个模式的关键。策略接口Strategy定义了一组算法的统一调用方式。它不关心算法具体怎么实现只约定算法的入参和返回值。这个接口的存在是策略能互相替换的前提。具体策略ConcreteStrategy实现了策略接口每一个具体策略类都封装了一种算法。各自内部完全可以做得不一样效率、风格、依赖再多对外始终是一致的。上下文Context持有一个策略接口的引用负责维护和调用当前策略。它可以把具体策略的设置交给外部调用方也可以在内部按条件选择策略。上下文不直接和具体策略类耦合只和策略接口打交道。这里要特别强调一点很多人会把策略模式里的上下文误当成业务代码里的“工具类”这是不对的。上下文是有状态的它代表的是“当前正在使用哪个策略”这一个事实。2.2 手写第一个策略模式支付示例我直接用一个支付场景来演示。假设你做了一个电商系统用户下单后需要支付目前支持支付宝、微信支付、银行卡支付三种方式以后还要拓展更多渠道。第一步定义策略接口。支付策略统一抽象成一个方法入参是订单金额和支付参数返回值是支付结果。接口只做签名约定不管内部走的是什么签名算法还是加密逻辑。public interface PayStrategy { PayResult pay(BigDecimal amount, PayParams params); }第二步实现三个具体策略。支付宝策略走支付宝SDK微信支付策略走微信SDK银行卡策略走银联接口。每个策略实现各写各的互相之间没有任何依赖。public class AliPayStrategy implements PayStrategy { Override public PayResult pay(BigDecimal amount, PayParams params) { // 调用支付宝支付的真正逻辑 return new PayResult(true, 支付宝支付成功, ordersn); } } public class WechatPayStrategy implements PayStrategy { Override public PayResult pay(BigDecimal amount, PayParams params) { // 调用微信支付的真正逻辑 return new PayResult(true, 微信支付成功, ordersn); } } public class BankCardPayStrategy implements PayStrategy { Override public PayResult pay(BigDecimal amount, PayParams params) { // 调用银联支付的真正逻辑 return new PayResult(true, 银行卡支付成功, ordersn); } }第三步写一个上下文类。上下文最重要的作用是屏蔽策略的持有逻辑。调用方只需要告诉上下文“我要用什么方式支付”上下文内部就把对应的策略拿出来执行。public class PayContext { private PayStrategy strategy; public void setStrategy(PayStrategy strategy) { this.strategy strategy; } public PayResult executePay(BigDecimal amount, PayParams params) { return strategy.pay(amount, params); } }第四步组装使用。用户从前端传过来一个支付方式代码后端根据代码选择对应的策略然后放进上下文执行。public class PayService { private PayContext context new PayContext(); public PayResult pay(String payType, BigDecimal amount, PayParams params) { PayStrategy strategy createStrategy(payType); context.setStrategy(strategy); return context.executePay(amount, params); } private PayStrategy createStrategy(String payType) { switch (payType) { case ALI: return new AliPayStrategy(); case WECHAT: return new WechatPayStrategy(); case BANK: return new BankCardPayStrategy(); default: throw new IllegalArgumentException(不支持的支付方式); } } }你看这个版本的支付逻辑已经比满屏if-else清爽多了。但我提前说一句上面的代码其实还不是最优解——createStrategy方法里那个switch会随着支付方式变多而变长这是接下来要解决的重点问题。3. 经典应用场景实战从if-else重构到策略模式3.1 恶心的if-else代码长什么样我早年在某个电商项目中接手过一个订单折扣模块当时那个方法大概长这样我简化了业务细节public BigDecimal calculateDiscount(Order order) { BigDecimal discount BigDecimal.ZERO; String userLevel order.getUserLevel(); if (VIP_GOLD.equals(userLevel)) { if (order.getAmount().compareTo(BigDecimal.valueOf(1000)) 0) { discount order.getAmount().multiply(BigDecimal.valueOf(0.85)); } else { discount order.getAmount().multiply(BigDecimal.valueOf(0.9)); } } else if (VIP_SILVER.equals(userLevel)) { discount order.getAmount().multiply(BigDecimal.valueOf(0.92)); } else if (NORMAL.equals(userLevel)) { if (order.isFirstOrder()) { discount order.getAmount().multiply(BigDecimal.valueOf(0.95)); } else { discount BigDecimal.ZERO; } } // 后面可能还有节假日折扣、满减折扣、会员日折扣... return discount; }这个方法的真实版本大概有两百多行里面有各种嵌套判断、状态标记、临时变量。每次需求方提一个新折扣规则我都得在这个方法里找插缝的位置别提多痛苦了。更要命的是这个方法是通用方法所有订单处理流程都在调它我改一个分支全链路都可能受影响。3.2 重构步骤抽出接口、封装策略、组装上下文重构的第一步先把折扣计算抽象成一个策略接口。注意接口的粒度要对入参是订单出参是折扣金额这个粒度就是最合适的。public interface DiscountStrategy { BigDecimal calculate(Order order); boolean supports(Order order); }接口里加了一个supports方法一会你就知道为什么这么设计。第二步把原来的每个分支变成一个独立的策略类。public class GoldUserDiscountStrategy implements DiscountStrategy { Override public BigDecimal calculate(Order order) { if (order.getAmount().compareTo(BigDecimal.valueOf(1000)) 0) { return order.getAmount().multiply(BigDecimal.valueOf(0.85)); } return order.getAmount().multiply(BigDecimal.valueOf(0.9)); } Override public boolean supports(Order order) { return VIP_GOLD.equals(order.getUserLevel()); } } public class SilverUserDiscountStrategy implements DiscountStrategy { Override public BigDecimal calculate(Order order) { return order.getAmount().multiply(BigDecimal.valueOf(0.92)); } Override public boolean supports(Order order) { return VIP_SILVER.equals(order.getUserLevel()); } }normal用户可以这么写public class NormalUserFirstOrderStrategy implements DiscountStrategy { Override public BigDecimal calculate(Order order) { if (order.isFirstOrder()) { return order.getAmount().multiply(BigDecimal.valueOf(0.95)); } return BigDecimal.ZERO; } Override public boolean supports(Order order) { return NORMAL.equals(order.getUserLevel()); } }第三步把从前的调用方法改成策略调度逻辑。找到一个支持当前订单的策略然后执行它。public class PromotionEngine { private ListDiscountStrategy strategies; public PromotionEngine(ListDiscountStrategy strategies) { this.strategies strategies; } public BigDecimal calculateDiscount(Order order) { DiscountStrategy matched strategies.stream() .filter(strategy - strategy.supports(order)) .findFirst() .orElseThrow(() - new IllegalArgumentException(没有找到匹配的折扣策略)); return matched.calculate(order); } }重构到这里你再看原来的调用方原来那两百行的判断逻辑全部消失了变成了维护一个ListDiscountStrategy的问题。以后新增一个“会员日折扣”只需要新写一个策略类加进列表里就行PromotionEngine一个字都不用改。3.3 工厂 策略让调用方不再关心分支刚才的PromotionEngine虽然简洁但还有一个隐含问题策略列表从哪来如果是在调用方手动new那调用方还是逃不掉创建策略的逻辑。更优雅的做法是把策略的创建过程收拢到一个工厂里。public class DiscountStrategyFactory { private static final ListDiscountStrategy STRATEGIES new ArrayList(); static { STRATEGIES.add(new GoldUserDiscountStrategy()); STRATEGIES.add(new SilverUserDiscountStrategy()); STRATEGIES.add(new NormalUserFirstOrderStrategy()); } public static ListDiscountStrategy getAllStrategies() { return Collections.unmodifiableList(STRATEGIES); } }然后在构造PromotionEngine的时候直接传入工厂的返回结果PromotionEngine engine new PromotionEngine(DiscountStrategyFactory.getAllStrategies());注意这个工厂的写法是静态初始化简单场景够用但如果你用了Spring这种框架这步直接用依赖注入那更舒服。这个我放到后面的实战经验里细说。这里的核心思想是策略模式负责把算法拆开工厂负责把策略组装起来。两个模式一组合代码的灵活度立刻就上来了。4. 用Map Lambda把策略模式写到极简4.1 用Map做策略注册表上面的实现每个策略一个类类文件会比较多。有人可能觉得我只是做个小逻辑搞这么多类是不是有点重这里有个更轻量的玩法用Map把“策略标识”和“策略实现”映射起来。还是支付那个例子先定义策略接口可以和之前一样然后不再写三个独立的策略类而是在一个配置类里直接把三个策略注册进Mappublic class PayStrategyRegistry { private final MapString, PayStrategy payStrategyMap new HashMap(); public PayStrategyRegistry() { payStrategyMap.put(ALI, params - doAliPay(params)); payStrategyMap.put(WECHAT, params - doWechatPay(params)); payStrategyMap.put(BANK, params - doBankPay(params)); } public PayStrategy get(String payType) { PayStrategy strategy payStrategyMap.get(payType); if (strategy null) { throw new IllegalArgumentException(不支持的支付方式: payType); } return strategy; } private PayResult doAliPay(PayParams p) { // 支付宝支付逻辑 return new PayResult(true, 支付宝支付成功, p.getOrderSn()); } private PayResult doWechatPay(PayParams p) { // 微信支付逻辑 return new PayResult(true, 微信支付成功, p.getOrderSn()); } private PayResult doBankPay(PayParams p) { // 银联支付逻辑 return new PayResult(true, 银行卡支付成功, p.getOrderSn()); } }这样使用的时候就是一个简单的方法调用PayStrategy strategy registry.get(ALI); PayResult result strategy.pay(amount, params);Map版本的策略模式把“创建策略”和“调用策略”都压缩进了一个注册表里逻辑一目了然非常推荐用在支付渠道、消息模板、导出格式这类枚举值固定的场景。4.2 用函数式接口压缩代码如果你连PayStrategy这个接口都不想定义Java 8的函数式接口可以直接救场。FunctionPayParams, PayResult就是一个现成的策略接口public class PayServiceLambda { private final MapString, FunctionPayParams, PayResult payFuncMap new HashMap(); public PayServiceLambda() { payFuncMap.put(ALI, this::doAliPay); payFuncMap.put(WECHAT, this::doWechatPay); payFuncMap.put(BANK, this::doBankPay); } public PayResult pay(String payType, BigDecimal amount, PayParams params) { params.setAmount(amount); return payFuncMap.getOrDefault(payType, p - new PayResult(false, 不支持的支付方式, null)) .apply(params); } }有人说这就不算策略模式了算而且是很地道的现代Java做法。策略接口不一定非要是一个自定义的interface只要具备“同一入口、不同实现、可替换”这三个特征都是策略模式。函数式接口让我们省掉了大量样板代码逻辑反而更紧凑。4.3 两种极简写法的取舍Map 方法引用这种方案最大的好处是类文件数量急剧下降一个注册类就能把事情讲清楚。缺点是每一个策略其实是一个私有方法策略之间的逻辑如果差别很大这个类会变得臃肿内聚性变差。接口 独立实现类的传统写法好处是每个策略独立成类可读性高后续扩展策略时不容易影响其他策略缺点则是类爆炸小项目里显得笨重。我的建议很简单一个策略只有几行逻辑用Map Lambda一个策略能写两百行老老实实拆成独立类。不要为了简洁而简洁策略模式的本质目标是降低复杂度的不是比拼代码行数。5. 策略模式的进阶玩法与注意事项5.1 用枚举实现策略我们都知道枚举本身就可以携带行为和实现接口这个特性用来实现策略模式很舒服。来看一个计算运费的小例子public enum FreightStrategy implements FreightCalculator { STANDARD { Override public BigDecimal calculate(Order order) { return BigDecimal.valueOf(10); } }, EXPRESS { Override public BigDecimal calculate(Order order) { return BigDecimal.valueOf(20).add(order.getAmount().multiply(BigDecimal.valueOf(0.01))); } }, OVERSEAS { Override public BigDecimal calculate(Order order) { return BigDecimal.valueOf(80).add(order.getAmount().multiply(BigDecimal.valueOf(0.05))); } }; Override public boolean supports(Order order) { return order.getFreightType().equals(this.name()); } }这个写法最大的好处是策略的选择直接通过枚举的valueOf或者name来匹配不用再单独写注册表。需要新增一种运费策略只需要在枚举里加一个常量。如果策略逻辑复杂依然可以专门实现一个抽象方法不改变枚举本身的框架。不过要注意枚举策略一旦定义是没法在运行时动态替换的——它的实例数量固定。如果你的业务场景对策略的动态性要求高比如某些规则在不停机时就能热切换那枚举就不合适还是得用Map那套。5.2 策略模式与状态模式的区别这是我在面试候选人时几乎必问的一个问题。策略模式和状态模式长得实在太像都是上下文持有接口、都通过接口调用方法、都由具体实现类承载逻辑但两者的动机完全不同。策略模式的核心是“算法替换”一组策略之间是平级的选择权在调用方策略本身不知道其他策略的存在。状态模式的核心是“状态迁移”对象内部的状态随着事件会发生变化状态之间的转换规则甚至由状态自己管理调用方只是触发行为并不知道也不可能指定状态。一句话分辨法如果你的代码里策略切换是靠外部条件决定的这是策略模式如果策略切换是靠当前策略执行后自己的结果推动的这是状态模式。5.3 策略模式与模板方法模式的区别模板方法模式解决的是“同一算法的骨架不变但某些步骤可以延后到子类实现”的问题。它强调的是复用骨架子类只需要填充个别步骤。策略模式强调的是整体算法可替换连骨架都不需要一致。比如说做报表导出。模板方法模式定义了一个导出流程查数据、格式化、填充模板、生成文件、发送通知其中“格式化”这一步不同报表可以不一样。策略模式则是直接定义ExportStrategy接口有Excel导出策略、PDF导出策略、CSV导出策略整个导出过程完全是另一套实现没有共用的骨架。很多项目里两个模式经常配合使用——模板方法控制大流程策略模式替换流程中的某一步。理解这两个模式的侧重点是灵活运用它们的前提。6. 策略模式的典型误用与避坑实录6.1 策略类数量膨胀怎么办策略模式最大的副作用就是当你真的遵循“一个算法一个类”时类数量会变得非常多。如果项目里已经有三十种折扣规则三十个类堆在一起也够头晕了。我常用的处理方式有两种。一是用内部静态类代替顶层类把同一业务域的策略放在一个外层类里代码定位快包路径也不会漂。二是用枚举策略或者Map Lambda代替独立类把短小的策略压缩进一个注册中心。如果你的策略实现只是几行规则真的没必要新建文件。但这里有个度的问题如果一个文件里塞了十几个短策略文件又开始变得难以浏览就得考虑按策略之间的关联性拆分成多个文件了。好的代码组织没有银弹关键看团队习惯和维护成本。6.2 上下文的状态管理陷阱策略模式里的上下文应该保持极简它只负责持有策略和调用策略。但有些开发者习惯在上下文里存业务参数例如把order、amount都塞进上下文字段。这会导致一个严重问题同一上下文对象被多个线程共享时策略状态会互相污染。看这个反面例子public class BadPayContext { private PayStrategy strategy; private BigDecimal amount; // 多线程共享时这里会出问题 }如果多个请求复用同一个上下文实例后一个请求的金额覆盖前一个线程不安全就会炸。正确的做法是上下文不持有具体业务数据业务参数直接通过方法参数传入。如果你确实需要每次调用是独立状态可以每次new一个上下文或者干脆用局部变量。6.3 策略接口设计的粒度问题策略接口的入参和出参粒度直接决定策略的可复用性。入参太具体比如直接传VipGoldOrder那这个策略只能服务一个场景换个场景就用不上了。入参太抽象比如直接传Object策略内部就得一大堆强转判断平行策略之间的耦合度就上来了。我的经验是找“领域模型的最基本抽象”。在下单、支付、促销场景里Order就是一个非常稳定的入参载体。在消息推送场景里Message接口或者通用的PushContext就是合适入参。出参也尽量用一个稳定的模型别用MapString, Object糊一层后续调用方炸裂时后悔都来不及。6.4 策略与工厂乱耦合有人把策略的创建逻辑写进策略接口本身让每个策略自己决定自己如何创建这是不推荐的。策略类应该专注于算法逻辑创建和注册交给工厂、容器或者注册表去处理职责分离各管一摊。否则新增一个策略要动的地方又不止一处策略模式带来的扩展优势就白白丢失了。7. 策略模式在真实框架里的影子7.1 排序算法里的策略思想Java开发最常见的Collections.sort(List, Comparator)其实就是策略模式的标准用法。Comparator是策略接口不同的比较器是具体策略Collections.sort是上下文。你把升序、降序、按某个字段比较等不同的策略传给同一个排序方法排序入口不用修改就能应对各种排序需求。ThreadPoolExecutor的RejectedExecutionHandler也是策略模式。四种拒绝策略塞给线程池线程池只是调用rejectedExecution方法根本不需要关心你是要丢弃还是要抛异常这就是策略模式的典型应用。7.2 Spring框架里的策略痕迹Spring的Resource抽象、BeanFactory、事件监听器这些其实都大量使用了策略思想。最典型的是HandlerMapping——Spring MVC中根据请求找到对应的Handler不同的映射策略可以替换。HandlerAdapter更明显一个适配器接口多种实现策略适配不同种类的处理器。我们自己在Spring项目里用策略模式最爽的场景就是把策略Bean注入一个MapComponent public class PayStrategyRouter { private final MapString, PayStrategy payStrategyMap; Autowired public PayStrategyRouter(MapString, PayStrategy strategyMap) { this.payStrategyMap strategyMap; } public PayStrategy route(String payType) { return payStrategyMap.get(payType); } }只要给每个策略Bean在注册时指定好名字Spring容器启动时就会自动把策略收集进Mapkey默认就是Bean名。你连工厂带注册表都省了这是现在Spring项目里我认为最推荐的策略模式打开方式。不过用这个方式要小心Map的key必须是唯一的两个策略Bean如果重名启动时就会冲突。实际项目中我会在策略类上用Component(ALI)这种方式显式指定Bean名避免默认类名截断产生的迷惑。7.3 我在项目中用得最爽的几个场景我个人的经验是策略模式最适合处理下面五类场景支付渠道不同渠道的搭建、回调、对账逻辑差异大策略 工厂的组合几乎成了标配。消息推送短信、邮件、App推送到各家的SDK和参数差异策略接口统一发送入口后面加新渠道就是加一个策略类。订单折扣/营销活动全场满减、单品促销、会员折扣、新人券每一种都是一个策略订单引擎只负责路由。数据导出格式Excel、PDF、CSV各写各的策略报表服务端到端扩展。微服务间的调用降级正常调用走A策略降级走B策略开关一改策略一换比满屏if (enableXxx)干净太多。8. 策略模式学习路线与源码阅读建议8.1 推荐的源码阅读顺序如果你想把策略模式消化成自己的东西光看我写的这份代码还不够建议去读三个源码片段。第一个是java.util.Comparator看接口怎么定义然后看Comparator.comparing这类默认方法怎么和lambda配合理解接口、函数式接口和策略在Java 8之后如何融合。第二个是RejectedExecutionHandler的四种现成实现体会在框架源码里策略类是怎么组织命名的怎么用小的类结构承载一个明确的行为。第三个是Spring的HandlerMapping或者HandlerAdapter看大厂框架里策略接口的粒度如何把控、策略的选择逻辑放在哪个环节、策略之间如何解耦。每读一个源码都反问自己三个问题策略接口为什么这么设计选择策略的逻辑为什么放在这里如果让我来写少了某个角色会有什么后果这三个问题想清楚掌握任何一个设计模式都只是时间问题。8.2 面试时怎么回答策略模式面试官问策略模式上来就把三点讲清楚一是策略模式的定义一组可互换的算法家族把算法的定义和使用分离二是三个角色的职责策略接口、具体策略、上下文各司其职三是核心价值消除条件分支满足开闭原则。紧接着给一个自己真实做过的案例最好是电商或者消息推送的。不用长讲清楚从if-else重构到策略模式的路径和重构前后的差异就够了。如果你能顺手点出策略模式和状态模式的边界再举一个JDK源码里的例子面试官基本会认定你是真正理解这个模式的人。9. 写在最后的实操体会我个人实际用策略模式这几年最大的体会是不要为了用模式而用模式。策略模式是消除坏味道的工具不是用来炫耀代码结构的装饰品。如果一个分支只有两三个逻辑也就几行你硬拆出四五个策略类反而让本来简单的事情变得绕来绕去。策略模式最好的切入时机是你发现同一个条件判断出现在了两个以上不同的调用方法里或者你的独立分支数量已经膨胀到了七八个并且每次新增分支都要疯狂翻上下文。这时候动手重构你会感受到设计模式真正的价值——改动面收敛到一个地方新增需求变成纯增量操作。最后分享一个小技巧写完策略代码后看一眼你的调用方再瞄一眼策略接口。如果调用方里还能看到具体策略类名说明你的上下文封装还不到位如果策略接口的任何改动会导致所有策略类大面积报错说明接口粒度还是不理想。好代码是一层一层磨出来的策略模式给了你一次把复杂业务按规则切成片的机会切得好不好全看你愿不愿意在设计上多花那半小时。这个系列的第一篇就先到这后续我会继续拆解其他Java设计模式每个模式都坚持用业务场景说话用源码实例佐证用避坑经验收尾。咱们下篇见。