
我到现在还记得那次代码评审的场景。活动上线前夜大家围在工位前看我这周刚写的支付模块整整200多行的pay()方法里密密麻麻排着24个if-else从支付宝到微信到银行卡到余额每个渠道一套逻辑。主管扫了两眼只丢下一句话改成策略模式吧。我当时心里是有点不服气的但后来把策略模式彻底吃透之后才明白这一句话背后藏着的是整个行为型设计模式里最实用、最贴近日常业务的一套思维方法。如果你正在学23种设计模式或者正在准备期末、应付大作业、重构自己那堆写满if-else的业务代码这篇文章就是给你准备的。我会把策略模式从定义到结构、从重构实战到和Spring结合、从踩坑记录到面试考点全部拆开讲配合完整可运行的Java代码和Python/C对照争取让你看完就能直接在自己项目里落地。1. 先讲一个真实翻车经历24个if-else引发的重构1.1 支付需求为什么会写成if-else地狱很多刚写业务代码的人都会有这种感觉需求一变代码就跟着膨胀。今天产品说加一个花呗支付明天说银行卡要支持信用卡和储蓄卡分开后天说余额支付要限制单笔上限。如果一开始就在一个方法里用if-else堆每加一个渠道就是加一个分支方法越来越长变量越来越多改一个分支担心影响其他分支测试时要重新梳理所有路径。我当时写支付就是这个状态。if (alipay.equals(payType))里套着十几行逻辑中间又有内部判断同级再来一个else if。表面上看每个渠道都是先验参数、再调远程接口、最后更新订单状态但代码就是没法抽出来因为所有逻辑都交织在一个大方法里像一碗浆糊。这里要解释一个关键问题为什么业务上看起来一样的操作在代码里会被写碎因为大家的直觉是先判断渠道再执行对应逻辑这是典型的面向过程的思考方式。而行为型设计模式里的策略模式恰恰是要改变这个思考路径。1.2 策略模式出场一句话讲清楚它是什么策略模式的定义听起来其实很简单定义一族算法把每个算法封装起来并且使它们之间可以互相替换。注意这个族字它意味着这些算法做的事情本质相同只是实现细节不一样。支付渠道是策略折扣计算是策略排序方式是策略登录校验方式也是策略。用大白话类比一下你要从北京去上海高铁、飞机、自驾都能到达你的目标是一样的但不同交通方式就是不同的策略。你不需要把坐高铁和坐飞机的逻辑全写在一个方法里而是应该各自封装成独立方案然后通过一个上下文对象去选择执行。这个思想落地到代码里就是三个角色Strategy定义公共接口声明算法方法。ConcreteStrategy实现接口每个具体策略都是一个独立类。Context持有策略引用负责调用策略方法。哪一天需要加新渠道不用再打开那个200行的大方法而是新建一个类实现接口在客户端换一个对象即可。新代码不碰旧代码这就是开闭原则最好的落地方式。2. 策略模式的结构拆解三个角色一个都不能少2.1 Strategy接口把变化抽成稳定契约Strategy接口是整个模式的定海神针。它存在的意义不是定义某个具体行为而是定义这一类行为的统一调用方式。比如支付场景里不管什么渠道最终都是给我一个金额你去把钱付了所以接口里就是pay(BigDecimal amount)。接口设计有一个注意点不要把业务特有的参数塞进公共方法签名里。比如支付可能有的渠道需要传用户ID有的需要传优惠券如果你在接口方法里把这些参数全列上就会出现大量用得着的参数在每个实现类里都被迫声明代码非常难看。通常做法是定义一个统一的请求/上下文参数对象把可选字段都放里面实现类按需取值。public interface PayStrategy { // 统一入口根据请求参数执行支付 void pay(PayRequest request); }接口的抽象粒度很关键。抽得太大实现类之间差别过大策略类各写各的策略模式就失去了可替换的意义抽得太小比如把验参下单调远程每个步骤都抽成策略调用方要组合多个策略复杂度反而上升。我的经验是一个场景入口对应一个策略接口不要追求接口方法的极致细粒度。2.2 ConcreteStrategy具体策略每个分支变成一个独立类具体策略类是真正干活的地方。拿支付举例支付宝策略、微信策略、银行卡策略各自实现接口。这样拆分最大的好处是每个类只关心自己那一套逻辑测试、修改、排查都变得非常清晰。有一个细节值得专门说具体策略类的状态管理。策略对象在Spring容器里默认是单例的所以你在策略类里绝对不能放本次调用独有的成员变量比如订单号、金额这类数据。一旦多线程并发进来不同请求的数据会互相污染。正确做法是策略类本身无状态所有临时数据都通过方法参数或请求对象传进来返回结果也通过返回值传递。public class AlipayStrategy implements PayStrategy { Override public void pay(PayRequest request) { // 1. 参数校验 if (request.getAmount() null || request.getAmount().compareTo(BigDecimal.ZERO) 0) { throw new IllegalArgumentException(支付金额必须大于0); } // 2. 调用支付宝SDK System.out.println(调用支付宝SDK支付金额 request.getAmount()); // 3. 更新订单状态 } } public class WechatPayStrategy implements PayStrategy { Override public void pay(PayRequest request) { // 微信支付逻辑 System.out.println(调用微信SDK支付金额 request.getAmount()); } }2.3 Context上下文持有策略的容器上下文对象是策略模式里容易被新手忽略的角色。很多人以为策略模式就是接口实现类其实没有Context客户端就得满世界拿着Strategy对象自己选自己调。Context的价值在于把策略的持有和使用统一收口对调用方屏蔽策略切换的细节。Context里一般有两个核心部分一个策略引用可以用字段保存一个执行方法内部调用策略。构造函数注入策略或者通过setter方法动态切换策略都可以。public class PaymentContext { private PayStrategy strategy; // 构造器注入创建时确定策略 public PaymentContext(PayStrategy strategy) { this.strategy strategy; } // setter注入运行时可切换策略 public void setStrategy(PayStrategy strategy) { this.strategy strategy; } // 统一执行入口 public void executePay(PayRequest request) { strategy.pay(request); } }客户端使用的时候非常舒服PayStrategy alipay new AlipayStrategy(); PaymentContext context new PaymentContext(alipay); context.executePay(request); context.setStrategy(new WechatPayStrategy()); context.executePay(request);3. 从if-else到策略模式一次完整重构实操3.1 重构前的代码一个方法里塞下所有渠道为了让你直观感受到重构的价值我先贴一段典型的反面教材。假设业务需求就是三选一支付宝、微信、银行卡支付。public void pay(String payType, BigDecimal amount, Long userId) { if (alipay.equals(payType)) { // 校验支付宝参数 if (amount null || amount.compareTo(BigDecimal.ZERO) 0) { throw new IllegalArgumentException(支付金额不合法); } // 调支付宝SDK System.out.println(支付宝支付 amount); // 更新订单状态 orderService.updateStatus(userId, PAID); } else if (wechat.equals(payType)) { // 校验参数 if (amount null || amount.compareTo(BigDecimal.ZERO) 0) { throw new IllegalArgumentException(支付金额不合法); } // 调微信SDK System.out.println(微信支付 amount); // 更新订单状态 orderService.updateStatus(userId, PAID); } else if (bankcard.equals(payType)) { // 银行卡逻辑 if (amount null || amount.compareTo(BigDecimal.ZERO) 0) { throw new IllegalArgumentException(支付金额不合法); } if (amount.compareTo(new BigDecimal(10000)) 0) { throw new IllegalArgumentException(银行卡单笔限额1万); } System.out.println(银行卡支付 amount); orderService.updateStatus(userId, PAID); } else { throw new UnsupportedOperationException(不支持的支付方式); } }你仔细品一下三段逻辑里参数校验、调SDK、更新状态这三件事每一件都在重复。而且如果订单状态更新逻辑发生变化三个分支全要改。加一个新渠道等于再复制一段并改一改这种代码改到后期就会成为所有人都不敢碰的屎山。3.2 重构后的代码坏味道被拆成清爽的类用策略模式重构第一步是定接口第二步是写实现类第三步写Context第四步在调用方做一个简单映射。我把完整代码贴出来你可以直接跑起来对比感受。// 请求参数对象 public class PayRequest { private BigDecimal amount; private Long userId; private String cardNo; // getter / setter 省略 } // 策略接口 public interface PayStrategy { void pay(PayRequest request); } // 支付宝策略 public class AlipayStrategy implements PayStrategy { Override public void pay(PayRequest request) { validateAmount(request.getAmount()); // 调用支付宝SDK System.out.println(支付宝支付 request.getAmount()); } private void validateAmount(BigDecimal amount) { if (amount null || amount.compareTo(BigDecimal.ZERO) 0) { throw new IllegalArgumentException(支付金额不合法); } } } // 微信支付策略 public class WechatPayStrategy implements PayStrategy { Override public void pay(PayRequest request) { validateAmount(request.getAmount()); System.out.println(微信支付 request.getAmount()); } private void validateAmount(BigDecimal amount) { if (amount null || amount.compareTo(BigDecimal.ZERO) 0) { throw new IllegalArgumentException(支付金额不合法); } } } // 银行卡策略有自己的限额校验 public class BankCardPayStrategy implements PayStrategy { private static final BigDecimal LIMIT new BigDecimal(10000); Override public void pay(PayRequest request) { validateAmount(request.getAmount()); if (request.getAmount().compareTo(LIMIT) 0) { throw new IllegalArgumentException(银行卡单笔限额1万); } System.out.println(银行卡支付 request.getAmount()); } }再配一个简单工厂或者映射表把字符串和策略对应起来。这一步非常重要如果直接把new AlipayStrategy()写在Controller里那调用方到处都是分支切换等于把if-else搬家了。public class PayStrategyFactory { private static final MapString, PayStrategy STRATEGIES new HashMap(); static { STRATEGIES.put(alipay, new AlipayStrategy()); STRATEGIES.put(wechat, new WechatPayStrategy()); STRATEGIES.put(bankcard, new BankCardPayStrategy()); } public static PayStrategy getStrategy(String payType) { PayStrategy strategy STRATEGIES.get(payType); if (strategy null) { throw new UnsupportedOperationException(不支持的支付方式 payType); } return strategy; } }调用方的代码变成三行public void pay(String payType, PayRequest request) { PayStrategy strategy PayStrategyFactory.getStrategy(payType); strategy.pay(request); orderService.updateStatus(request.getUserId(), PAID); }注意重构后更新订单状态变成了公共代码放在调用方不再在每个分支里重复。这一步体现了重构的核心价值把重复干掉把变化隔离。3.3 为什么说开闭原则在这里体现得最彻底重构后的代码最迷人的地方在于加需求不改旧代码。如果下周产品说加一个云闪付你只需要做两件事新建YunShanFuPayStrategy实现接口。在工厂Map里加一行STRATEGIES.put(ysf, new YunShanFuPayStrategy())。原来的PayStrategy接口没动支付宝和微信的类没动调用方没动。这就是对扩展开放、对修改封闭的开闭原则。但这里有一个容易被误会的点策略模式并没有完全消灭if-else它只是把如果是什么类型就执行什么逻辑这个判断从业务代码里转移到了工厂/注册表里。工厂里那个Map.get()本质上还是分支选择只不过用哈希查找替代了字符串compare。这恰恰是合理的脏地方让所有变化集中在唯一一个入口而不是散落在成百上千个业务方法中。4. 策略模式的实战应用场景支付、折扣、排序与游戏AI4.1 电商促销折扣策略一个最经典的入门案例支付只是策略模式的一小块应用电商系统里折扣计算是另一个教科书级场景。你有没有见过这种需求平时不打折活动期间满100减20VIP用户打8折新人首单减50叠加起来还可能有大促满减规则。如果这些算法全部写进一个calculatePrice()方法那这个方法迟早变成一个改一次伤一次的重灾区。用策略模式怎么拆定义一个DiscountStrategy接口里面一个纯函数BigDecimal apply(BigDecimal originalPrice)。每种优惠规则一个实现类NoDiscountStrategy原价返回、FullReductionStrategy做满减计算、VipDiscountStrategy做打折、NewUserDiscountStrategy做固定减。然后由Context来决定用哪个策略。更重要的是多种优惠叠加时可以把多个策略组合起来逐个应用这一点比写复杂的嵌套if要清爽太多了。4.2 支付路由与第三方渠道真实线上系统的经典用法支付系统里有一个更高阶的用法叫支付路由。用户发起一笔支付系统要根据用户所处环境、金额大小、渠道可用性、费率成本来决定到底走哪条通道。这时候策略模式就不只是处理一个类型对应一个类了而是配合规则引擎每一条通道就是一个策略策略里可以自定义我是否适用这笔订单的判断。举个例子大额支付走银联渠道费率低小额支付走微信费率划算且某些渠道在深夜有维护窗口不接单。每条通道策略都实现支持当前订单吗 执行支付两个能力。路由算法不停尝试策略列表找到第一个可用的执行。这样新接入一个第三方渠道就是加一个策略类完全不用改路由逻辑。4.3 游戏AINPC行为策略切换很多学设计模式的人都会看游戏源码来加深理解因为游戏里的行为变化太丰富了。角色在地图上有多种状态巡逻、追击、攻击、逃跑。用if-else去判断如果玩家在视野内就追击如果血量低就逃跑写出来是能跑但策划要调整AI行为时代码改起来会痛不欲生。用策略模式来做每个行为巡逻、追击、攻击、逃跑都是一个策略类Agent持有一个BehaviorStrategy字段。当游戏事件触发时Agent动态设置策略agent.setBehavior(new AttackStrategy()); agent.update();这里注意游戏AI里策略模式常常跟有限状态机FSM配合使用。策略负责怎么做状态机负责什么时候切换两者组合起来用会比单用模式优雅很多。4.4 其他经典场景压缩算法、日志输出、数据校验策略模式的应用面比想象中广。压缩工具里zip、gzip、7z是三种压缩策略日志组件的控制台输出、文件输出、远程输出是三种策略参数校验场景里手机号校验、邮箱校验、身份证校验也完全可以各自封装成策略类。甚至JDK里最有名的比较器Comparator接口就是一个策略接口Collections.sort()接收不同比较器就是不同排序策略的注入。我给你一个判断是否适合用策略模式的万能标准你的代码里有没有一群做同一件事但做法不同的分支如果有且这些分支在可预见的未来还会继续增加那就果断上策略模式。如果只有两个稳定分支永远不变那写个if其实没多大问题。模式不是越多越好滥用模式比不用模式更可怕。5. 策略模式和其他模式怎么区分别再傻傻分不清5.1 状态模式 vs 策略模式别把切换和流转搞混状态模式和策略模式的代码结构几乎一模一样都是一个上下文类一个接口一堆实现类。区别在于谁来决定切换以及切换是否受当前状态影响。策略模式是外部选择客户端在调用前明确告诉Context我要用哪个策略策略之间是平级的没有先后顺序。状态模式是内部流转Context内部维护一个当前状态状态对象自己决定下一个状态是什么外部无法强行指定。举个例子订单状态有待支付、已支付、已发货、已完成谁能控制下一个状态只有当前状态自己知道你不能把待支付直接改成已完成因为状态对象内部的next()逻辑不允许。而支付渠道策略呢调用方说我今天想用微信就用微信明天想用支付宝就用支付宝完全是由外部决定的。5.2 模板方法模式 vs 策略模式继承还是组合模板方法模式是在父类里定义算法骨架把某些步骤延迟到子类实现策略模式是把整个算法封装为对象通过组合方式动态替换。一个是继承部分重写的套路一个是接口整体替换的套路。具体业务里两者常常混合使用策略接口的某个实现类内部可以用模板方法模式定义自己的执行骨架。比如AlipayStrategy内部先验参、再调SDK、再落库三步固定但调SDK这一步不同渠道差异很大那这个策略类本身也可以抽一个抽象父类把公共步骤固定下来把调SDK抽象成子类实现。模式和模式之间不是非此即彼而是可以嵌套组合的。5.3 工厂模式、命令模式与策略模式的分工很多人学到这里会乱工厂模式不是也能消除if-else吗命令模式不也是把请求封装成对象吗这三分到底怎么区分我的理解是这样的工厂模式关心对象怎么创建策略模式关心算法怎么执行命令模式关心请求怎么记录和撤销。你要买一台电脑去了联想工厂工厂负责按照型号生产这就是工厂模式你到了店里销售员根据你的预算给你推荐某款机型并拿给你这就是策略模式你下完单后想撤销保留一张退货命令卡这就是命令模式。实际开发中策略模式通常和简单工厂或注册表一起出现一个负责找策略一个负责执行策略。6. 经验之谈这些坑我都替你踩过了6.1 策略类膨胀问题策略注册表能救你策略模式最大的槽点就是类太多一个支付渠道一个类十种规则十个类再算上工厂、Context项目里到处都是新文件。我自己用着用着也发现很多策略类逻辑只有两三行完全没必要单独建文件。这时候可以换一种轻量玩法策略注册表。一个枚举 一个Map字段把策略名映射到Lambda表达式实现类就不需要单独建了。Java 8的Lambda在这个场景下特别好用public class PayStrategyRegistry { private static final MapString, PayStrategy STRATEGIES new HashMap(); static { STRATEGIES.put(alipay, req - System.out.println(支付宝支付 req.getAmount())); STRATEGIES.put(wechat, req - System.out.println(微信支付 req.getAmount())); } }如果策略逻辑复杂到超过三行再抽为独立类。这个取舍没有绝对标准我的原则是一个类里只做一件事但如果做事的代码只有一行那就别单独建类。6.2 客户端还是绕不开if-else怎么办有一种情况很尴尬虽然策略内部解耦了但选择策略的代码还是充满了if-else。比如如果订单金额大于1000用银行卡小于1000用微信这种选择逻辑本身就是业务规则它不该被消灭只该被收拢。解决思路有三条把规则放到工厂里客户端只传参数不传类型。策略接口增加boolean support(PayRequest request)方法由策略自己声明我能不能处理这笔订单路由代码循环遍历所有策略找到第一个support()返回true的。用规则引擎处理极度复杂的路由不过这就超纲了大多数业务用前两条足够。6.3 和Spring结合把策略注入成Map真实项目里没人手动new策略对象都是Spring容器管理。Spring有一个很妙的玩法把接口的所有实现类自动收集到一个Map里key是Bean名字Service public class PayService { Autowired private MapString, PayStrategy strategyMap; public void pay(String payType, PayRequest request) { String beanName payType Strategy; PayStrategy strategy strategyMap.get(beanName); if (strategy null) { throw new UnsupportedOperationException(不支持的支付方式 payType); } strategy.pay(request); } }这样连手动注册Map都省了以后新增策略类只需要加一个带Service注解的类业务代码一行都不用改。我在实际项目里特别喜欢这种写法因为它把新增扩展的成本降到了最低。唯一要注意的是Bean命名必须统一规范比如都叫xxxStrategy否则Map的key对不上。6.4 常用技巧空对象策略和Lambda简化策略模式里有一个变体叫空对象模式本质是一个什么都不做的策略类。比如折扣计算里没有优惠活动时不应该返回null而应该返回一个NoDiscountStrategy让调用的地方可以无脑执行省去一堆if (strategy ! null)的判断。另外在Python里策略模式可以写得更简洁。因为Python函数是一等公民不需要每个策略都定义类直接用匿名函数或普通函数就够了def apply_discount_vip(price): return price * 0.8 def apply_discount_normal(price): return price strategies { vip: apply_discount_vip, normal: apply_discount_normal, } price strategies[user_type](original_price)C则可以用std::function结合注册表来实现核心思路一样接口约束变成了函数签名约束。6.5 策略模式不是万能的什么时候别用最后必须泼一盆冷水。如果你的算法只有两种且永远不会变比如登录时判断密码对还是错这种简单二元逻辑上策略模式纯属浪费。策略模式的成本在于接口设计、实现类管理、调用方选择逻辑维护这些都有自己的复杂度。没有变化的地方不需要设计我见过很多过度设计的项目一个接口三个实现类但新增实现类的速度两年都赶不上一次这种模式就变成了为了模式而模式。还有一个反模式要警惕如果策略之间存在依赖关系比如老用户折扣必须在VIP折扣基础上叠加就不适合把每个策略变成平级类互相替换更适合用责任链模式或者装饰器模式去串联处理。策略模式处理的是互斥选择不是组合叠加搞清楚这一点能避免很多设计事故。7. 常见问题与排查技巧实录我踩过的那些坑写策略模式代码的过程中有几个问题真的是反复出现包括我自己带的实习生也经常中招。整理一个小表方便你排查时快速对照问题现象根因解决方案并发下策略对象数据错乱策略类里放了可变的成员变量策略类保持无状态数据一律走方法参数新增策略后调用方还在走老逻辑忘记更新工厂Map使用Spring的Map自动注入减少手动注册客户端大量if-else选择策略选择逻辑没有收拢移到工厂或增加support()方法让策略自行匹配策略接口定义过粗所有实现类都在复制相同的代码抽象粒度不合理引入抽象父类或模板方法提取公共流程多个策略要组合执行时互相干扰把策略模式用在了需要叠加的场景换成装饰器模式或责任链模式策略执行时出现空指针没有处理好null策略使用空对象策略避免返回null切换策略后旧策略未释放忘记移除Set或Map里的旧引用用Spring的Prototype作用域或显式清理再分享两个排查的经验技巧。第一个是断点先看策略类是否被正确注入我在Spring环境遇到过strategyMap注入为空的情况排查到最后是Bean没有加Service注解。第二个是接口新加方法后所有策略实现类都必须同步实现这个如果用Java的抽象类或者接口定义编译器就会强制检查但如果你用的是Lambda表达式注册方式编译期是不会发现新增方法的运行到了才报错这种情况要格外小心测试覆盖。另外如果是在准备设计模式期末或者大作业我建议你亲手写一遍策略模式不要只抄网上的代码。自己动手写一遍if-else重构为策略模式的过程比看十篇博客都有用。你甚至可以拿自己平时写的项目里的一个具体方法来做实验这个方法只要能满足一堆分支做同一件事这个条件就能体验完整的重构流程。写完之后看看Git diff你会直观感受到代码结构的变化。最后再分享一个我自己总结的心法策略模式的本质是面向接口编程思想在行为层面的具象化。它不生产任何新能力只是把代码中原本就存在的算法族重新组织了一下。理解了这个之后你会发现设计模式的学习并不需要记住每一个类的名字和构造而是明白当一个项目里变化的维度很多时怎么把变化封装起来让不变的代码稳定地依赖抽象。我后来写代码时的习惯是先问自己这段逻辑里什么是稳定的、什么是会变的然后把会变的东西用接口隔离开来这其实就是策略模式最核心的价值。希望这篇文章能帮你把策略模式真正用起来而不是停留在概念层面。