做后端有些年头的朋友,大概率都见过这种“祖传代码”:一个方法里塞了几十个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 上下文对象 PriceCalculator
Context 的角色需要持有策略接口,并提供一个设置策略的方法。代码里通常长这样:
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 Map<String, 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、UnionPayStrategy - Context:
PayService,持有当前选中的支付策略
用户在支付页选择一个渠道,后端根据渠道 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 Map<String, 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 名称就是aliPayStrategy,channelCode 传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 List<String> couponIds; // getter / setter 省略 }接口定义为calculate(OrderContext context)。这样将来加参数时,只需要在OrderContext里加字段,所有策略类的方法签名都不用改。这是我的核心建议之一:策略接口的入参要面向变化设计,不要为了省事只传一个最基础的参数。
7.5 不要把策略模式的策略配置写死在代码里
业务规则里的魔法数最忌讳写死在类里。折扣率 0.8、满减门槛 200、立减金额 50,这些在业务方眼里都是可以随时调整的活动参数。如果写死在策略类里,业务方改个数字,你就得重新发版。
正确做法是策略类从外部接收参数。在 Spring 项目里可以结合配置中心,把策略参数以配置项下发给服务,策略类构造时读取最新值。或者策略参数放在数据库配置表里,运行时查询。模式本身不规定参数怎么管理,但实际工程中这是避不开的问题。
重要:策略模式擅长组织算法,不擅长管理配置。算法可以用类封装,但算法需要的阈值、折扣率、有效期,一定放到配置层。把这两层分开,整个方案才真正可用。
8. 结尾:我用了这么多次策略模式之后
策略模式可能是所有设计模式里“性能比”最高的一个——学起来不难,用起来见效快,几乎不需要引入额外框架。但要说清楚,它不是什么高端技巧,它就是软件开发里“把变和不变拆开”这个朴素思想的落地。
我们团队现在有个不成文的约定:写业务代码,只要看到某段逻辑明显在按类别分支,并且类别的数量未来大概率会增长,就先停下来想一想——这里要不要用策略模式。判断标准不是“能不能用”,而是“该不该用”。两个策略都用 if-else 可能还行,五六个策略还在堆 if-else,那基本是在给后面的人挖坑。
如果这篇文章能让你在写代码时多一次思考:“这个分支以后会怎么变化”,那就比我单纯教你几个策略类怎么写有价值得多。设计模式不是背出来的,是踩坑踩出来的。真到了某天你看着自己跑冒烟的服务,翻代码日志准备通宵排障的时候,就会理解策略模式设下的边界到底替你挡了多少雷。