拓冰建站拓冰建站
首页 / 资讯中心 / 正文

策略模式实战:从if-else到支付场景的优雅重构

策略模式这个老话题网上讲的人不少但很多教程要么堆概念、要么贴几个简单例子就跑真正能把“为什么这样写”“什么时候该用”“跟别的模式怎么区分”讲清楚的并不多。我这些年不管是在电商项目里处理价格计算、还是做多通道消息推送、甚至是写审批流的状态机都被策略模式救过好几次命所以想从实际落地的角度把策略模式原原本本拆开讲一遍。先说这篇文章适合谁。如果你是还没出校门的学生正在准备设计模式期末考试或者课程大作业这篇文章可以帮你把策略模式的脉络理顺防止跟简单工厂、状态模式搞混如果你已经工作了几年写了不少业务代码但总觉得自己的if-else分支长得没法看这篇文章同样能给你一套可以直接抄走的改造方案。我会从最基础的毛病讲起用支付场景贯穿全程再补上Spring环境下如何跟依赖注入配合的实战写法最后把最容易踩的坑和容易混淆的边界全部列出来。1. 策略模式到底在解决什么问题策略这个词听起来很高大上其实说白了就是“同一个事情有多种做法每种做法封成一个独立的类让它们可以互相替换”。这个思想在业务代码里特别常见只是很多时候我们没意识到自己已经在用了或者说用得太随意写成了一堆不好维护的分支。1.1 从一段让人头疼的支付代码说起假设你在做一个商城系统用户下单以后要选择支付方式可能是微信支付、支付宝支付、银行卡支付。很多新手拿到这个需求第一版代码往往是这样的public class OrderService { public void pay(Order order, String payType) { if (wechat.equals(payType)) { // 微信支付相关逻辑 50 行 System.out.println(使用微信支付 order.getAmount()); // 拉起支付、回调处理、记录日志…… } else if (alipay.equals(payType)) { // 支付宝支付相关逻辑 50 行 System.out.println(使用支付宝支付 order.getAmount()); // 签名校验、异步通知、对账…… } else if (bankcard.equals(payType)) { // 银行卡支付相关逻辑 50 行 System.out.println(使用银行卡支付 order.getAmount()); // 跳转网银、验签、退款…… } else { throw new IllegalArgumentException(不支持的支付方式); } } }这段代码在功能上是没错的刚写出来甚至觉得很清爽不就是三个if嘛。但问题会随着业务迭代慢慢暴露第一个月接入了百度钱包加了第四个分支第二个月要改微信支付的SDK版本得钻进这个50行的分支里操作第三个月产品说支付完成后要增加积分赠送又要给每个分支都加上调用第四个月你发现单元测试想单独验证支付宝逻辑得把整个OrderService构造出来准备一堆无关的依赖。这种代码的毛病不是“能不能跑”而是“改起来痛不痛”。每动一次需求你都要在这一个方法里摸一遍全部逻辑而且你动微信那块代码的时候手指稍微一抖就可能影响支付宝的流程。这是典型的违背开闭原则——对扩展开放、对修改关闭。加一个支付方式你要修改已有的类而不是新增一个模块。1.2 策略模式的核心定义与角色划分策略模式的标准定义是定义一系列算法把它们一个个封装起来并且使它们可相互替换。这个定义里的关键词是“算法”但放到业务场景里不一定非得是什么排序、加密算法只要是“某种可独立替换的规则或行为”都可以当成策略。一个完整的策略模式有三个角色。策略接口负责定义这个规则家族的统一行为。它是整个模式的核心约束决定了所有策略类长什么样。比如PayStrategy定义一个pay方法那么不管微信还是支付宝都必须有这个入口。具体策略类就是每一种做法的具体实现。微信支付一个类支付宝一个类每个类独立完成自己的逻辑类与类之间互不干扰。上下文类持有当前使用哪个策略的引用负责把用户的请求转交给对应的策略对象。它不关心底层策略是怎么实现的只负责调用。这么一看策略模式的核心价值就非常清楚了把变化的逻辑从固定的业务流程里抽出来变化的部分单独成类固定不变的部分留在上下文里让两边各自演化。1.3 为什么说策略模式是在做“行为委派”理解策略模式有个特别好的角度它本质上是把“做什么”和“怎么做”拆开了。业务流程是命令的发起者它只关心“我要支付”但不关心“你用什么方式支付”策略类是命令的执行者它关心“我到底怎么支付”但不管业务方是谁。这种拆法让上层业务代码不用背负具体实现细节也让具体实现细节不用被迫跟着上层逻辑绑定在一起。我以前给新人讲这块喜欢用“导航App”来类比。你跟导航说“我要从A去B”这是业务意图导航根据当前路况、交通方式可能推荐驾车路线、地铁方案、骑行方案这些就是不同的策略。导航主体就是上下文而路线推荐算法就是策略你随时可以切换策略但导航框架完全不用改动。业务代码里的支付逻辑、折扣计算逻辑、消息推送逻辑本质上都是在处理这种“同一意图、不同实现”的问题。2. 用支付场景手把手实现一遍策略模式理论说得再热闹不如写一段能跑的代码实在。这一节我带着你从零把支付模块改成策略模式每一步都会告诉你为什么要这样写以及哪里容易踩坑。2.1 第一步抽出策略接口策略接口是整套方案的基石它定义了所有支付方式必须遵守的契约。这里最忌讳的是接口设计得太大、太具体把每种支付方式的特殊需求都塞进去那样具体策略类实现起来就会很痛苦。public interface PayStrategy { void pay(BigDecimal amount); }我把接口限定得非常简单就一个按金额支付的方法。实际项目里你可能还需要回调地址、订单号、用户ID等参数这时候可以定义个PayContext对象统一传进去而不是让接口方法越长越长。接口里提前想好“哪些是所有策略共有的行为”这是接口设计的第一课。有人可能会问微信支付和支付宝的签名规则完全不一样接口怎么统一这正是策略模式的价值——它只保证调用姿势一致至于里面是不是调用了完全不同的SDK策略模式不关心。你们各自在自己的具体策略类里折腾互不干扰。2.2 第二步实现三个具体策略类有了接口接下来是每个支付方式各写一个类。这里有个细节类的命名直接体现业务语义不要用什么PayStrategyA、PayStrategyB这种毫无信息量的名字。命名清晰代码的可维护性天然就高一半。public class WechatPayStrategy implements PayStrategy { Override public void pay(BigDecimal amount) { // 这里是微信支付的完整逻辑 System.out.println(微信支付金额 amount.toPlainString()); // 调用微信SDK、生成二维码、异步回调处理…… } } public class AlipayStrategy implements PayStrategy { Override public void pay(BigDecimal amount) { // 这里是支付宝的完整逻辑 System.out.println(支付宝支付金额 amount.toPlainString()); // RSA2签名、表单提交、验签…… } } public class BankCardStrategy implements PayStrategy { Override public void pay(BigDecimal amount) { // 这里是银行卡支付的完整逻辑 System.out.println(银行卡支付金额 amount.toPlainString()); // 跳转网银页面、等待回调…… } }每个策略类都只需要关心自己那摊事。微信支付要处理微信私钥和回调支付宝要做RSA2验签银行卡涉及到网银跳转它们之间没有任何代码层面的耦合。同一时间改微信的代码绝对不会碰坏支付宝这就是把变化隔离在单一策略类里带来的直接好处。2.3 第三步定义上下文类承接策略上下文是策略模式的调度中心。它看起来很简单但设计得好不好直接决定了调用方写起来舒服不舒服。public class PaymentContext { private PayStrategy strategy; public PaymentContext(PayStrategy strategy) { this.strategy strategy; } public void executePay(BigDecimal amount) { strategy.pay(amount); } }上下文的职责有两个保存当前策略的引用把请求转发给策略。有些初学的人会把上下文的职责无限放大又是做参数校验又是记录日志这其实偏离了它的定位。上下文应该是轻量的、专注的真正复杂的业务逻辑应该在具体策略类里。到了这一步原来业务方的代码就变得非常清爽public class OrderService { public void pay(Order order, PayStrategy strategy) { PaymentContext context new PaymentContext(strategy); context.executePay(order.getAmount()); } }调用方自己决定用哪个策略然后扔给上下文去执行。原来那堆if-else全部消失新增支付方式的时候OrderService连碰都不用碰只要新增一个策略类就好。这就是开闭原则的效果也是策略模式最直接的收益。2.4 关于“简单工厂模式”来解决if-else这件事这里要专门提一下很多人在用策略模式的时候会发现一个问题策略模式把每个分支的实现拆开了但调用方在外部还是要根据用户的选择去new不同的策略这个选择的过程本身又是一层if-else。PayStrategy strategy null; if (wechat.equals(payType)) { strategy new WechatPayStrategy(); } else if (alipay.equals(payType)) { strategy new AlipayStrategy(); }这种场景就需要简单工厂模式来兜底。简单工厂跟策略模式是天然的搭档工厂负责“根据参数创建正确的策略对象”策略模式负责“封装变化的行为逻辑”。两者不矛盾甚至是相辅相成的。所以网上的设计模式热词里才会把“策略模式、简单工厂模式”并列它们是组合拳。public class PayStrategyFactory { public static PayStrategy getStrategy(String payType) { switch (payType) { case wechat: return new WechatPayStrategy(); case alipay: return new AlipayStrategy(); case bankcard: return new BankCardStrategy(); default: throw new IllegalArgumentException(不支持的支付类型 payType); } } }这样一来原来散落在业务代码里的创建逻辑被收拢到了工厂一个类里。业务方只需要调用PayStrategyFactory.getStrategy(payType)拿到策略对象后交给上下文执行。这里的工厂只是一个创建工具不属于策略模式的标准结构但它解决了一个很实际的问题所以项目里这么组合几乎是标配。不过在Java生态里更优雅的做法是把整个工厂干掉直接用Spring的Bean容器来做策略查找。这也是实战里最常见的用法后面我会专门展开讲。3. 策略模式的实战进阶结合Spring做策略自动装配培训班和教科书里一般讲到第三步就结束了但真实的大型项目里根本不会让你new一个策略对象所有Bean都是Spring容器托管的。你需要掌握的是用Spring的依赖注入来替代手工工厂让策略的选择和装配变得自动、解耦。3.1 用ApplicationContext容器按类型获取策略Bean最简单直接的Spring风格写法是在需要策略的地方注入ApplicationContext然后根据策略类型从容器里取Bean。这种做法的好处是完全不依赖手工new所有策略类都是Spring Bean可以随时注入别的依赖。Service public class OrderService { Autowired private ApplicationContext applicationContext; public void pay(Order order, String payType) { // 约定Bean名称和payType一致 PayStrategy strategy (PayStrategy) applicationContext.getBean(payType); strategy.pay(order.getAmount()); } }为了让上面的代码能跑通每个策略类的Bean名称必须跟payType匹配。Spring默认会把WechatPayStrategy这样的类名首字母小写注册为Bean即wechatPayStrategy所以如果你传进来的payType是wechatPayStrategy就能直接命中。但这种约定属实太隐晦了不推荐在正经项目里用。3.2 更稳妥的方式策略注解Map自动注入我自己的团队在项目里用的是一套更直观的方式给每个策略类加一个注解标明它属于哪种类型然后利用Spring把同一接口的所有策略注入到一个Map里。Component public class PayStrategyFactory { private final MapString, PayStrategy strategyMap new ConcurrentHashMap(); public PayStrategyFactory(ListPayStrategy strategies) { for (PayStrategy strategy : strategies) { if (strategy instanceof WechatPayStrategy) { strategyMap.put(wechat, strategy); } else if (strategy instanceof AlipayStrategy) { strategyMap.put(alipay, strategy); } else if (strategy instanceof BankCardStrategy) { strategyMap.put(bankcard, strategy); } } } public PayStrategy getStrategy(String payType) { PayStrategy strategy strategyMap.get(payType); if (strategy null) { throw new IllegalArgumentException(不支持的支付类型 payType); } return strategy; } }核心点在于构造器注入ListPayStrategySpring会把容器中所有PayStrategy接口的实现类都收集起来传进这个参数。之后在方法里按类型判断塞进Map调用方就能通过payType快速拿到对应的策略。这个写法的好处是所有策略类都还是普通的Spring Bean它们可以正常依赖别的服务同时工厂本身也很轻量。不过这段代码里还有一点不够优雅——用instanceof判断类型还是有点硬编码的味道。更工程化的做法是引入一个业务类型注解让策略自己声明它负责什么类型工厂通过元数据自动注册这样一来新增策略的时候工厂代码一行都不用改。我后面会专门提这个进阶思路。3.3 Spring下策略模式的实际调用效果经过Spring整顿之后业务方的调用会变成这样RestController public class OrderController { Autowired private PayStrategyFactory strategyFactory; PostMapping(/pay) public String pay(RequestBody PayRequest request) { PayStrategy strategy strategyFactory.getStrategy(request.getPayType()); strategy.pay(request.getAmount()); return 支付已发起; } }Controller层不关心支付到底怎么实现只负责接收参数、获取策略、发起调用。策略类自己也不关心Controller长什么样、参数从哪来只负责处理属于自己的支付逻辑。中间的工厂只是做了一次很薄的类型映射。我在实际项目中特别喜欢这种结构的另一个原因是对单元测试极度友好。测试支付逻辑时只需要构造对应策略类的实例注入mock的依赖就能单测某一种支付方式的完整行为再也不用为了测支付宝去启动整个Spring上下文。4. 策略模式与相近设计模式的关键区分设计模式学到后期最大的难点已经不在于会用某个模式而在于面对一个业务场景时选对模式。策略模式经常跟简单工厂、状态模式、模板方法模式混在一起讲很多期末考题和大作业也喜欢让你比较这些模式。这里我把自己总结的区分思路分享出来。4.1 策略模式和简单工厂模式的区别这是最常见的一组混淆。简单工厂解决的是“对象的创建”问题策略模式解决的是“行为的封装和替换”问题。一个是创建型模式一个是行为型模式从根上就不同。从代码层面看简单工厂关注的是怎么根据参数new出正确的对象它返回的是产品策略模式关注的是几个算法怎么组织、怎么替换它操作的是已经创建好的具备相同接口的对象。所以前面才说两者天然能组合使用用简单工厂来管理策略对象的创建用策略模式本身来管理策略行为的使用。打个比方简单工厂像是一个自动售货机你按一个按钮它弹出一罐可乐策略模式像是你手里有一副可以随意换的耳机插头换成Type-C还是Lightning你的听歌行为不变。操作的对象和解决的问题不是一个维度。4.2 策略模式和状态模式的区别状态模式在日常编码里没那么高频但它的命名很容易让人跟策略模式产生联想毕竟都是一堆类、都实现了同一个接口、都能动态切换。二者的本质差异在于策略模式的切换是调用方主动发起的而状态模式的切换是状态对象自己发起的。具体来说策略模式中的上下文不知道当前策略的内部逻辑外部业务通过参数决定用哪个策略选择权在调用方状态模式中上下文和状态对象之间是深度绑定的状态对象内部维护着状态的流转规则当前状态处理完以后会自动切换到下一个状态。状态模式适合写订单状态机、审批流程、电梯运行逻辑等场景策略模式适合做算法替换、计价规则组合、支付方式选择等场景。我见过最典型的误用案例是有人拿策略模式去写订单的状态流转结果发现策略之间看不出先后顺序上下文还得自己维护一大堆状态切换逻辑越写越乱最后才改成状态模式。4.3 策略模式和模板方法模式的取舍关系模板方法模式的核心是定义一个算法骨架把其中某些步骤延迟到子类实现。它和策略模式看着像但注意区分模板方法强调的是“骨架固定、局部可变”策略模式强调的是“整体行为可替换”。模板方法模式下子类不得不继承父类逻辑绑定在类层次里策略模式下策略之间没有继承关系它们通过接口统一起来可以自由组合。如果你的算法流程是固定的只有中间某一步有多种做法优先考虑模板方法如果整个算法流程在不同场景下都可能不一样优先考虑策略模式。有些复杂的场景两者也会结合模板方法定义流程框架框架内部的某个环节通过策略模式替换这种混合使用在实际项目中一点也不罕见。5. 策略模式使用中的典型陷阱与常见问题策略模式能解决很多问题但它不是银弹。我觉得作为一个成熟的开发者比学会写策略模式更重要的是学会判断什么时候不要用策略模式以及用了之后要警惕哪些坑。5.1 陷阱一策略爆炸策略模式鼓励把每种变化单独成类但如果变化维度太多策略类会爆炸式增长。比如一个价格策略按客户等级分VIP、普通按购买渠道分线下、线上按支付方式分现金、积分、优惠券几种维度一组合策略类可能有十几个二十个。这种时候硬用策略模式反而让代码变得碎片化不好维护。我的应对建议是先分析变化维度之间是否有稳定的正交结构如果有考虑用组合模式或者把规则封装成策略内部的一个配置属性而不是一维一策地建类。5.2 陷阱二策略类持有大量共享状态有些人在一个策略类里放了全局可变的字段比如计数器、临时缓存。策略对象如果被Spring默认以单例方式托管这些状态在多线程并发下就会出问题。同一时间多个请求在用同一个策略实例里面的字段互相污染查半天查不到原因。我的处理原则是策略类尽量做成无状态的所有方法入参传入数据方法内部局部变量完成计算。如果确实需要缓存把缓存放到独立组件里而不是放在策略字段上。5.3 陷阱三过度设计新手学完策略模式后很容易陷入“万物皆策略”的狂热连两个if分支都要抽成几个接口和类。这等于给一只蚂蚁搭了个宫殿代码数量翻了几倍可读性和维护性反而下降。我给自己定的尺度是当分支数量还在2到3个以内且每个分支逻辑不足20行时不要上策略模式先用清晰的if-else或者switch把话说明白。当分支数量增长到4个以上或者每个分支的实现越来越复杂或者你已经出现了两三次“因为改动一个分支而牵动其他分支”的情况时再动手重构为策略模式。重构要趁早但更要挑对时机。5.4 常见问题速查表下面这张表我在带团队时用来做设计评审的checklist现在分享出来大家可以对照检查自己的策略模式落地情况。症状可能原因处理建议新增策略时还要改工厂代码instanceof或if-else判断写死在工厂结合注解和反射/Spring启动后扫描自动注册策略类之间出现了大量重复代码抽象粒度不对共性逻辑没抽取考虑在接口和具体策略间增加抽象模板类上下文类代码越来越臃肿上下文承担了策略选择的职责之外的事把选择逻辑下沉到工厂上下文保持纯净策略数量膨胀到十几个变化维度过多设计粒度太细使用规则配置组合策略而不是一个规则一个类策略切换失败但业务已经执行缺少策略前置校验工厂层做合法性校验提前抛出明确异常6. 策略模式在大项目中的扩展玩法如果前面讲的是策略模式的标准姿势这一节算是我自己在实际项目里摸索出来的扩展玩法可以让你对策略模式的理解再上一个台阶。6.1 结合Spring的BeanPostProcessor实现策略自动注册前面工厂写法里提到了注解加自动注册。具体的实现方式是用Spring的BeanPostProcessor在容器初始化每个策略Bean之后读取它类上标注的注解把Bean放进策略Map里。这样新增策略类只需要写一个类加一个注解工厂完全不用改动。Target(ElementType.TYPE) Retention(RetentionPolicy.RUNTIME) Component public interface PayType { String value(); }然后在每个策略类上标注PayType(wechat) public class WechatPayStrategy implements PayStrategy { // ... }再写一个后置处理器在Spring容器扫描到这些Bean时自动把它们注册进策略工厂Component public class PayStrategyRegistry implements ApplicationListenerContextRefreshedEvent { Resource private ApplicationContext applicationContext; private MapString, PayStrategy strategyMap new ConcurrentHashMap(); Override public void onApplicationEvent(ContextRefreshedEvent event) { MapString, PayStrategy beans applicationContext.getBeansOfType(PayStrategy.class); beans.values().forEach(strategy - { PayType payType strategy.getClass().getAnnotation(PayType.class); if (payType ! null) { strategyMap.put(payType.value(), strategy); } }); } }这种玩法在中小型项目中可能有点杀鸡用牛刀但在策略数量持续增长的平台上非常有用。我见过一个配置中心类的系统策略种类超过40种后面每增加一种对接渠道都只需要新增一个类、标一个注解其余代码一行不动团队协作效率提升非常明显。6.2 策略模式与函数式接口的结合Java 8之后很多传统策略模式的实现可以大幅简化。当策略接口只有一个抽象方法时可以直接用函数式接口和Lambda表达式来实现策略省掉一大堆策略类文件。public interface DiscountStrategy { BigDecimal calculate(BigDecimal originalPrice); }调用时直接LambdaDiscountStrategy studentDiscount price - price.multiply(BigDecimal.valueOf(0.8)); DiscountStrategy vipDiscount price - price.multiply(BigDecimal.valueOf(0.5));如果再配合策略工厂的Map甚至可以做成配置驱动MapString, DiscountStrategy strategies new HashMap(); strategies.put(student, price - price.multiply(BigDecimal.valueOf(0.8))); strategies.put(vip, price - price.multiply(BigDecimal.valueOf(0.5)));这种方式对简单策略场景非常优雅但复杂策略还是建议单独立类因为Lambda无法承载复杂的内部逻辑和多行协作。6.3 给大作业或期末复习的速记提醒如果你是学生朋友正在复习设计模式准备期末我最后送你一份策略模式的考试记忆卡片。类型行为型设计模式。目标定义算法族分别封装让它们可以互相替换使算法的变化独立于使用算法的客户。核心角色:Strategy、ConcreteStrategy、Context。关键特征策略模式强调的是行为的可替换性借助接口实现。典型应用场景支付方式选择、价格计算、消息发送渠道、数据校验规则。优缺点优点是符合开闭原则、避免多重条件判断、便于扩展缺点是类数量增加、客户端必须了解各策略的差异才能正确选择。跟工厂模式的组合记忆工厂负责选对象策略负责换行为。设计模式的学习本来就是一条漫长的路策略模式是你掌握行为型模式的第一块跳板把它彻底弄懂后面再去看观察者、状态、责任链接受速度快得多。我自己在实际项目里踩过的坑基本都是“用错了场景”而不是“模式本身有毛病”所以临了再多说一句千万记住模式是工具不是目的业务场景永远应该先于模式选择被考虑清楚。这套思路你不管是用在课堂上还是工作中都不会走偏。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门