策略模式实战:从if-else地狱到优雅的算法替换
发布时间:2026/10/14 21:37:49 锦皓数字建站

做后端有些年头的朋友大概率都见过这种“祖传代码”一个方法里塞了几十个if-else每增加一种业务规则就往里再插一个分支改了三次要么把原先的逻辑带崩了要么自己都绕晕了。去年年初我接手过一个订单系统重构里面算价格的方法就是这样前后加起来将近两百行switch嵌套if新同事想加一种会员折扣愣是改了三个版本才跑对。后来把这块拆成策略模式整个过程差不多只花了半天改完再看清爽得不像同一个项目。策略模式属于行为型设计模式里最基础、也最常用的一类。它解决的核心就是“算法可以互相替换”这件事把每种算法封进独立类里让调用方在不关心内部实现的前提下切换行为。今天这篇不打算照着教科书把 UML 图搬一遍我想用接项目的视角从需求场景、手写实现、代码优化、模式辨析再到实际落地的坑把策略模式讲透。适合正在学设计模式的学生也适合被 if-else 折磨的初中级后端开发。1. 从一段膨胀的计费代码说起策略模式到底治什么病1.1 一个典型的“祖传方法”长什么样假设你在做一个电商项目订单结算时按用户类型和订单金额算折扣。刚开始很简单普通用户原价VIP 用户打 9 折之后就慢慢加需求——满减、会员日双倍积分、新用户立减、内部员工价……于是代码开始变成下面这样public double calcPrice(String userType, double amount, boolean isNewUser, int dayType) { double price amount; if (normal.equals(userType)) { // 普通用户无折扣 } else if (vip.equals(userType)) { price amount * 0.9; // 还有别的逻辑... } else if (svip.equals(userType)) { if (dayType 1) { // 会员日 price amount * 0.8; } else { price amount * 0.85; } } if (isNewUser) { price price - 20; } // 满减逻辑、优惠券逻辑、员工价逻辑... if (price 100) { // 一些边界处理 } return price; }这段代码看着眼熟吧它的问题不在“功能能不能跑”而在“能不能一直跑下去”。我在实际项目里见过比这更夸张的版本计费方法里同时处理了会员折扣、满减、优惠券、运费、地区加价、活动补贴七八种规则互相嵌套方法体接近 300 行单元测试根本没法完整写。1.2 膨胀背后的三个结构性痛点如果你只是吐槽“这代码丑”那说明还没看到本质。三个痛点才是关键第一违反开闭原则。每加一种规则就要修改原有方法而不是新增独立代码。改老代码就意味着可能破坏原有行为线上出过好多次“加了员工折扣结果普通用户不打折了”这种事故。第二业务规则无法复用。折扣算法和调用方强绑定散落在各个方法里。订单系统里算价格用这套逻辑购物车系统里又复制粘贴了一份改规则时就得满项目找漏一处就线上出 bug。第三难以测试。分支太多想要覆盖所有组合情况测试用例指数级增长。更麻烦的是规则之间有隐含顺序改一个分支可能影响后续所有判断排查问题全靠断点慢慢打。策略模式解决的核心就是把“算法是什么”和“算法用在哪”拆开。“算法是什么”封装成一个个策略类“算法用在哪”由上下文对象统一管理。调用方不再关心今天是满减还是折扣它只需要知道“帮我算一个价格”就行。2. 策略模式的核心设计三个角色与两层解耦2.1 三个角色的职责划分策略模式只有三个角色结构简单到刚学的人都会怀疑“就这么点东西能解决问题”Strategy抽象策略定义算法族统一的公共接口。一般来说是个接口或抽象类里面声明一个方法比如calculate(double amount)。这个接口就是“算法约束”所有策略都必须实现它。ConcreteStrategy具体策略实现抽象策略接口的类。每个类封装一种具体的算法。折扣策略就写折扣的计算逻辑满减策略就写满减的计算逻辑互不干扰。Context上下文持有抽象策略的引用负责调用策略方法。它不实现任何具体算法只是把请求转发给当前持有的策略对象。Context 存在的意义是隔离变化客户端不需要知道策略内部怎么算也不需要在每次调用时指定算法参数。三个角色对应到刚才的计费场景接口就是PriceStrategy具体类就是各种价格策略上下文是一个PriceCalculator。2.2 为什么这种设计能治住 if-else要理解策略模式的价值得想明白一个点原来的代码里“选择算法”和“执行算法”是揉在一起的。调用calcPrice方法时你要先判断用户什么类型、活动什么状态这就是“选择算法”选完之后执行一堆计算这就是“执行算法”。策略模式相当于把这两件事拆开了。选择算法的逻辑被挪到客户端或工厂类里执行算法的逻辑被封闭进各个策略类。这样一来新增一种折扣方案只需要新增一个策略类然后告诉外部“我这里多了一种策略”原来所有if-else加分支的地方一个都不用动。这就是对修改关闭、对扩展开放。两层解耦分别指什么第一层是调用方与算法的解耦调用方只面向接口编程不再依赖具体实现类。第二层是策略内部逻辑的解耦满减策略不知道折扣策略存在更不需要互相判断。各管各的出了问题只改一个类测试也只需要测这一个类。注意策略模式不是“消灭 if-else”的银弹。真正消灭的是“算法内部的分支”而不是“策略选择处的分支”。你依然需要一个地方决定“今天是 VIP 用户用 VIP 策略”这个选择逻辑要么留在客户端要么抽到一个工厂里不可能凭空消失。后面讲实战时会展开。2.3 和三大设计原则的关系策略模式是体现“组合优于继承”的典型。原来想复用一段算法逻辑很自然想到继承父类、重写方法。但继承是静态绑定的一个类只能有一个父类想同时拥有多种算法组合就很尴尬。策略模式把算法变成可组合的成员对象运行时想换就换灵活性完全不在一个层级。面向接口编程这一点在这个模式里体现得也特别直白。Context 持有的不是具体策略而是抽象策略这样任何遵守契约的新策略都能无缝接入客户端甚至感知不到变化。我在团队里跟新人讲这个模式时经常用一句话概括把变的部分抽出去把不变的部分定下来。变的部分是具体算法不断扩展不变的部分是接口定义和上下文转发逻辑保持稳定。3. 手写一个策略模式从需求到代码的完整过程3.1 需求与接口定义用一个最简单、但能覆盖完整流程的场景演示设计一个订单价格计算器支持原价、折扣、满减三种价格策略。现在项目里实际设计时我不会一上来就写接口而是先把需求列清楚。这里就是三条规则原价策略价格不变折扣策略打指定折扣比如 0.8 就是八折满减策略满 200 减 50然后定义接口。接口方法设计有个细节参数尽量别只传一个金额因为将来可能有会员积分、优惠券叠加等需求。第一次设计就只传一个金额后面往外扩展时接口改动会波及所有策略类。先按单一职责来只算价格public interface PriceStrategy { double calculate(double amount); }3.2 三种具体策略类的实现每个策略类只关心自己负责的算法。原价最简单直接返回原值public class NormalPriceStrategy implements PriceStrategy { Override public double calculate(double amount) { return amount; } }折扣策略需要一个折扣率参数通过构造方法传入public class DiscountPriceStrategy implements PriceStrategy { private final double discountRate; public DiscountPriceStrategy(double discountRate) { this.discountRate discountRate; } Override public double calculate(double amount) { return amount * discountRate; } }满减策略把规则写在算法里public class FullReductionPriceStrategy implements PriceStrategy { private final double threshold; private final double reduction; public FullReductionPriceStrategy(double threshold, double reduction) { this.threshold threshold; this.reduction reduction; } Override public double calculate(double amount) { if (amount threshold) { return amount - reduction; } return amount; } }这三个类各自独立新增一个“新用户立减”策略时只需要新增类不用碰已经存在的代码。这就是最直观的扩展性。3.3 上下文对象 PriceCalculatorContext 的角色需要持有策略接口并提供一个设置策略的方法。代码里通常长这样public class PriceCalculator { private PriceStrategy strategy; public PriceCalculator(PriceStrategy strategy) { this.strategy strategy; } public void setStrategy(PriceStrategy strategy) { this.strategy strategy; } public double calculate(double amount) { return strategy.calculate(amount); } }注意两个方法构造方法用于初始化策略setStrategy用于运行时切换策略。这是策略模式的一个核心能力——运行时的行为切换。用户今天可能是普通用户明天升级成 VIP程序运行到一半直接把策略替换掉不需要重建整个计算器。3.4 客户端调用与演示客户端负责选择策略上下文负责执行策略public class Client { public static void main(String[] args) { PriceCalculator calculator new PriceCalculator(new NormalPriceStrategy()); double price1 calculator.calculate(300); System.out.println(原价策略 price1); calculator.setStrategy(new DiscountPriceStrategy(0.8)); double price2 calculator.calculate(300); System.out.println(八折策略 price2); calculator.setStrategy(new FullReductionPriceStrategy(200, 50)); double price3 calculator.calculate(300); System.out.println(满200减50策略 price3); } }输出结果原价策略300.0 八折策略240.0 满200减50策略250.0这样看策略模式的核心用法其实很简单。但光会照着写是不够的实际项目里策略的选择通常不是手动set一下这么简单而是由一个选择器根据业务参数决定。这个后面在“与工厂模式结合”和“实际应用案例”里详细说。4. 让代码更简洁Java 8 Lambda、枚举与工厂的进阶玩法4.1 为什么策略模式适合用 Lambda 简化Java 8 之后如果策略接口只有一个抽象方法那它就是一个函数式接口。函数式接口意味着什么意味着你可以直接用 Lambda 表达式替代策略类的实例化不用再写一堆没有状态的实现类。先看原来写一个策略类的过程定义一个类、实现接口、写方法、然后 new 出来一堆样板代码。现在定义接口时保证它只有一个方法即可FunctionalInterface public interface PriceStrategy { double calculate(double amount); }客户端调用方式可以变成PriceStrategy discount amount - amount * 0.8; PriceStrategy fullReduction amount - amount 200 ? amount - 50 : amount;这种情况特别适合策略没有内部状态、不需要持有额外参数初始化的场景。原来是三分之一屏代码现在一行搞定。注意Lambda 简化虽然香但不能滥用。策略逻辑一旦超过几行或者需要较多参数建议还是建独立策略类保持可读性和可测试性。Lambda 用多了团队里有人看不懂维护成本反而不降反升。4.2 把策略塞进枚举或 Map替代策略选择处的 if-else很多教程到这里就停了但实际项目中你面临最多的问题是“怎么根据一个 code 找到对应策略”。新手最容易的还是写 if-else比如if (discount.equals(type)) { strategy new DiscountPriceStrategy(0.8); } else if (fullReduction.equals(type)) { strategy new FullReductionPriceStrategy(200, 50); }这不又回去了嘛。比较优雅的两种方式第一种用 Map 注册策略。在上下文或一个独立配置类里提前把所有策略放进去public class StrategyRegistry { private final MapString, PriceStrategy strategyMap new HashMap(); public StrategyRegistry() { strategyMap.put(normal, amount - amount); strategyMap.put(discount, amount - amount * 0.8); strategyMap.put(fullReduction, amount - amount 200 ? amount - 50 : amount); } public PriceStrategy get(String type) { return strategyMap.getOrDefault(type, amount - amount); } }这样选择策略就变成了一次 Map 查找时间复杂度 O(1)扩展型策略只需要往 Map 里 put 一条。第二种用枚举管理策略类型。枚举天然适合做类型与策略的映射而且写代码时类型安全不容易传错字符串public enum StrategyType { NORMAL(normal, new NormalPriceStrategy()), DISCOUNT(discount, new DiscountPriceStrategy(0.8)), FULL_REDUCTION(fullReduction, new FullReductionPriceStrategy(200, 50)); private final String code; private final PriceStrategy strategy; StrategyType(String code, PriceStrategy strategy) { this.code code; this.strategy strategy; } public PriceStrategy getStrategy() { return strategy; } public static PriceStrategy fromCode(String code) { for (StrategyType type : values()) { if (type.code.equals(code)) { return type.strategy; } } return NORMAL.getStrategy(); } }这种写法的好处是新增一种策略在枚举里加一个常量就行连注册代码和选择代码都不用动。项目的业务计费规则比较固定时我优先推荐枚举方案。4.3 策略模式与工厂模式各司其职的分工这两个模式经常被放在一起用很多人搞混。简单说清楚分工工厂模式负责“怎么创建策略”策略模式负责“怎么执行算法”。客户端需要某个策略时不直接 new 策略类而是通过工厂询问“给我一个满减策略”。工厂内部可能根据参数决定返回哪个类的实例或者从注册表里取一个现成的实例。这样选择逻辑被收拢到一个地方上下文和客户端都不用反复写类型判断。看一个最简单的简单工厂public class StrategyFactory { public static PriceStrategy create(String type) { if (discount.equals(type)) { return new DiscountPriceStrategy(0.8); } else if (fullReduction.equals(type)) { return new FullReductionPriceStrategy(200, 50); } return new NormalPriceStrategy(); } }用工厂之后客户端只需要和工厂打交道上下文只需要和策略接口打交道整个链路各司其职。选择逻辑虽然表面看还是 if-else但它被收拢到唯一的工厂类里不会散落到处都是这已经是很大的改良。而且工厂内部的 if-else 还能继续用 Map 或枚举优化。5. 策略模式与几个“长相相似”的模式辨析5.1 与状态模式结构像动机完全不同状态模式和策略模式的类结构几乎一样都是上下文持有另一个对象运行时都可以切换这个对象。很多人会在这里犯迷糊我当年学的时候也绕了一下。关键区别在切换由谁决定。状态模式的切换通常是对象内部自己触发的因为状态变了行为就跟着变比如订单状态从“待支付”切到“已支付”这个切换是订单内部完成的调用方没有主动选择。策略模式的切换是外部触发的调用方在几个独立算法中显式选择一种比如根据用户类型决定用哪种折扣。换个角度理解状态模式强调“随时间而变”策略模式强调“随选择而变”。实际开发里我见过把策略模式当成状态模式用的情况——这也能跑但语义表达不正确代码在维护时会误导后来人。用错模式比不用模式还麻烦选型时一定要想清楚“变化的原因”到底来自内部还是外部。5.2 与模板方法模式组合替换 vs 继承骨架模板方法模式用继承来实现算法复用基类定义算法骨架子类重写其中的某些步骤。策略模式用组合来实现算法替换整个算法都封装在一个策略对象里。举个例子做报表导出模板方法模式会定义一个抽象类它规定了“读数据 - 格式化 - 输出”三大步子类分别实现这三大步的具体细节。策略模式则会把“导出 csv”和“导出 excel”各自做成策略类上下文直接持有一个导出策略。选型建议如果多个算法有共同的执行骨架、只是部分细节不同用模板方法更合适如果算法整体独立、互相不依赖细节用策略模式更好。现代开发更偏好组合所以策略模式的使用频率更高。5.3 与桥接模式结构上的“亲属关系”桥接模式是结构型模式解决的是“抽象和实现两个维度独立变化”的问题比如跨平台 UI 组件不同操作系统有不同实现。它的结构是两个接口层级抽象部分和实现部分通过桥接接口连接。策略模式是行为型模式关注行为的封装和替换。单看代码桥接和策略的设计有时很像但意图不一样。一句话总结桥接分离的是“这件事有几个维度”策略分离的是“这件事有几种做法”。判断维度是否正交是区分的关键。做实际项目不用死记这些区别遇到问题时优先问自己我的变化来源是什么——是算法选择还是状态流转还是平台维度答案会自动把你引向正确的模式。6. 实际项目应用案例策略模式怎么落进真实业务6.1 案例一多渠道支付接入电商系统对接支付渠道时各种支付方式的接口、参数、验证逻辑完全不同。如果让订单模块直接依赖所有支付渠道的 SDK订单模块就会爆炸。用策略模式设计抽象策略接口PayStrategy定义pay(Order order)方法具体策略AliPayStrategy、WechatPayStrategy、UnionPayStrategyContextPayService持有当前选中的支付策略用户在支付页选择一个渠道后端根据渠道 code 从策略注册表取出对应策略调用pay方法。后续新增一个支付渠道只需要新增策略类和注册配置订单模块一行不改。这套设计我在项目中反复用过每次新增渠道时都只需要在注册表里加一行非常省心。注意支付场景里策略类往往不是无状态的——可能内部持有 SDK 客户端这时就要考虑 spring 容器的 Bean 作用域后文会说。6.2 案例二营销活动引擎的折扣规则营销活动的玩法层出不穷限时折扣、会员折上折、满额立减、新客立减、老客回馈……业务方每周都可能抛过来一个新玩法。用策略模式可以把每个玩法封装成一个策略类再通过配置中心下发策略参数。实际落地时玩法和策略是一一映射关系。前端传一个 activityType 字符串后端通过注册表拿到对应策略将配置参数传给策略对象执行。这里要特别提醒策略模式本身只解决了“算法怎么封装、怎么替换”策略的参数化管理并不包含在模式里。比如满减的“满 200 减 50”200 和 50 这两个参数应该从配置中心读取而不是写死在代码里。实际项目中我会把策略参数设计成一个统一的 Json接口定义成calculate(OrderContext)让策略自己从上下文里取它需要的参数。6.3 案例三Spring 项目中策略 Bean 的注入方式纯 Java 的策略模式实现是教学用的真实 Spring 项目里有更工程化的做法。核心就是利用 Spring 的依赖注入把实现同一接口的所有 Bean 自动注入到一个 Map 里Service public class PayService { Autowired private MapString, PayStrategy strategyMap; public void pay(String channelCode, Order order) { PayStrategy strategy strategyMap.get(channelCode); if (strategy null) { throw new IllegalArgumentException(不支持的支付渠道: channelCode); } strategy.pay(order); } }Spring 会把PayStrategy接口的所有实现类注入到这个Map里key 默认是 Bean 名称。比如AliPayStrategy的 Bean 名称就是aliPayStrategychannelCode 传aliPayStrategy就能直接命中。更多时候会用自定义注解给 Bean 起别名或者把 Bean 名约束成和业务 code 一致。这种注入方式的优点是一旦有新的策略实现类加入 Spring 容器Map里自动多一个元素连注册代码都不用写。这已经是目前 Spring 项目里最主流的策略组织方式强烈推荐在需要用到 Spring 的项目中使用。7. 常见问题与避坑实录7.1 策略类数量膨胀管理成本上升策略模式用好后一个业务下可能有十几个甚至几十个策略类。类一多管理就费劲新人看着整个策略包会发懵。我的做法是按业务域拆包比如payment.strategy、promotion.strategy、notify.strategy不要让所有策略堆在一个包里。其次每个策略类命名要带上业务含义比如MemberSummerCampActivityStrategy一目了然。如果策略类本身就是无状态且只有一行 lambda 能表达的逻辑直接用枚举值或 lambda 注册不必强行建类。策略膨胀到一定程度就要回顾一下是不是策略粒度过细。有些“策略”之间其实只有参数差异可以合并成一个策略类用不同的配置参数构造不同实例。比如会员折扣和节日折扣算法核心都是乘法只是折扣率不同完全可以用同一个DiscountPriceStrategy(discountRate)搞定。7.2 策略对象的线程安全问题这是一个很容易被忽略的坑。策略类如果无状态内部只有方法逻辑、没有实例字段那所有线程共享一个实例完全没问题这也是最理想的情况。但如果策略类里存了可变状态比如支付渠道的 SDK client 或者用户相关的临时数据就有并发风险。解决方案有两条第一还是尽量把策略类设计成无状态所有入参通过方法参数传进来。第二如果策略必须持有状态在 Spring 容器中把 Bean 的作用域设为prototype每次获取都是新实例。用 Map 注册策略的时候也要注意如果手动持有的是有状态策略实例并发场景可能出错。这里我踩过一个实打实的坑某个策略类里放了一个ThreadLocal存临时数据结果线上高并发时各种串数据排查了两天才定位到是策略实例被多线程共享了。后来把临时数据全部移出策略类问题立刻消失。7.3 策略选择处仍然出现的 if-else不能视而不见前面说过策略模式不消灭选择处的 if-else但这不代表你可以任由它发展。几个策略时用 if-else 还没什么感觉十几个策略时中间那个选择方法又会变成新的“祖传方法”。所以一定要给策略的选择逻辑单独设计一层比如策略工厂或策略注册表。让所有选择逻辑收拢到一个地方并且用 Map 或枚举去优化查找方式。这是策略模式从“会写”走向“写好”的关键一步。另外还要拒绝一种偷懒改法在策略类的内部又用 if-else 判断“我这个策略要不要生效”。这会让策略类和外部条件耦合失去独立性。策略类应该只关心算法本身是否启用它是选择层的事。7.4 参数化策略时接口方法的参数设计要留余地我在教学里用的calculate(double amount)足够简单但真实业务里价格计算可能需要用户等级、商品品类、会员积分、优惠券、活动时间等多个信息。如果你接口一开始就限定calculate(double amount)后期扩展就会很痛苦。更健壮的做法是定义一个上下文参数对象public class OrderContext { private double amount; private int userLevel; private boolean isNewUser; private ListString couponIds; // getter / setter 省略 }接口定义为calculate(OrderContext context)。这样将来加参数时只需要在OrderContext里加字段所有策略类的方法签名都不用改。这是我的核心建议之一策略接口的入参要面向变化设计不要为了省事只传一个最基础的参数。7.5 不要把策略模式的策略配置写死在代码里业务规则里的魔法数最忌讳写死在类里。折扣率 0.8、满减门槛 200、立减金额 50这些在业务方眼里都是可以随时调整的活动参数。如果写死在策略类里业务方改个数字你就得重新发版。正确做法是策略类从外部接收参数。在 Spring 项目里可以结合配置中心把策略参数以配置项下发给服务策略类构造时读取最新值。或者策略参数放在数据库配置表里运行时查询。模式本身不规定参数怎么管理但实际工程中这是避不开的问题。重要策略模式擅长组织算法不擅长管理配置。算法可以用类封装但算法需要的阈值、折扣率、有效期一定放到配置层。把这两层分开整个方案才真正可用。8. 结尾我用了这么多次策略模式之后策略模式可能是所有设计模式里“性能比”最高的一个——学起来不难用起来见效快几乎不需要引入额外框架。但要说清楚它不是什么高端技巧它就是软件开发里“把变和不变拆开”这个朴素思想的落地。我们团队现在有个不成文的约定写业务代码只要看到某段逻辑明显在按类别分支并且类别的数量未来大概率会增长就先停下来想一想——这里要不要用策略模式。判断标准不是“能不能用”而是“该不该用”。两个策略都用 if-else 可能还行五六个策略还在堆 if-else那基本是在给后面的人挖坑。如果这篇文章能让你在写代码时多一次思考“这个分支以后会怎么变化”那就比我单纯教你几个策略类怎么写有价值得多。设计模式不是背出来的是踩坑踩出来的。真到了某天你看着自己跑冒烟的服务翻代码日志准备通宵排障的时候就会理解策略模式设下的边界到底替你挡了多少雷。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。