我先问一个很现实的问题:你是不是也写过那种几十个分支的if-else或者switch-case,每次产品经理过来说“加个新规则”,你都得在方法里偷偷补一段逻辑,生怕影响其他分支?如果有,那么设计模式里的策略模式大概率就是你要找的那个解药。这篇文章我不打算讲太多教科书式的大道理,而是结合 Java 和 C++ 的实战代码,把策略模式从头到尾拆一遍:包括它到底在解决什么问题、三个角色的职责划分、完整重构过程、常见误用,以及我实际踩过的坑。无论你是在准备期末考试、做设计模式大作业,还是已经在项目里被变更需求折磨得头大,这篇都适合你。
1. 策略模式到底在解决什么问题
1.1 先从一堆 if-else 说起
很多人第一次接触策略模式,是被《Head First 设计模式》里那个鸭子例子搞懵的:一会儿 FlyBehavior,一会儿 QuackBehavior,好像很抽象,跟手上的业务代码完全对不上。我换个场景你马上就能懂。
假设你写了一个订单计费系统,目前有三种用户等级:普通用户、会员、VIP。计费逻辑长这样:
public double calculate(double price, String userLevel) { if ("normal".equals(userLevel)) { return price; } else if ("member".equals(userLevel)) { return price * 0.9; } else if ("vip".equals(userLevel)) { return price * 0.8; } return price; }第一版只有三行,看着还行。但接下来产品经理说:大客户打七折、节假日叠加九五折、新用户首单五折、内部员工特殊折扣……你会发现这个方法的 body 会越来越长,每个折扣的计算规则互相纠缠,改一个分支可能影响另一个分支。最可怕的是,测试的时候要把每个分支都覆盖一遍,而每次新增分支,回归的范围都指向同一个方法。
这个问题的本质是什么?是“算法”和“使用算法的客户端”被硬耦合在一起了。策略模式做的事情,就是把一组可以互相替换的算法封装起来,让客户端可以独立于算法本身去使用它们。
1.2 三个角色的职责划分
策略模式的标准结构只有三个角色,一定要记牢:
| 角色 | 类名习惯 | 职责 |
|---|---|---|
| 策略接口 | DiscountStrategy | 定义算法族的统一入口,通常是一个抽象方法 |
| 具体策略 | NormalDiscount、MemberDiscount | 各自实现一套算法,互不干扰 |
| 上下文 | OrderCalculator | 持有策略接口引用,负责调用策略,也可能负责切换策略 |
用一句话概括:上下文是“老板”,策略是“员工”,老板不关心员工具体怎么干,只关心你干的活能不能满足接口约定的要求。
类图不需要死记,你只需要记住依赖方向:上下文依赖策略接口,具体策略也实现策略接口,上下文中绝对不要直接 new 某个具体策略类。一旦你在上下文里写了某个具体策略类的名字,这个模式的扩展性就废了一半。
1.3 为什么“多用组合,少用继承”在这里最有说服力
设计模式里有条很出名的原则叫“Favor composition over inheritance”,很多新手不理解。策略模式就是最好的例子。
如果你用继承去实现折扣逻辑,会怎么做?定义一个VIPUser继承User,然后重写calculate方法。这看起来很自然,但问题在于:折扣是一个容易变化的维度,用户身份又是一个容易变化的维度,两个维度叠加起来,继承树就会爆炸。比如“VIP用户 + 节假日 + 新客首单”,你怎么继承?总不能为每种组合都搞一个子类吧?
策略模式换了一个思路:把“变化的部分”(折扣算法)单独抽出来,用组合的方式注入到上下文里。身份还是那个身份,但是算钱的时候,你可以随时给这个订单换上不同的折扣策略。这样两个维度的变化就解耦了。这就是策略模式最核心的思维:把变化封装起来,用组合替代继承。
2. 动手实现:一个计费接口的完整重构过程
2.1 需求场景:一个刚需的计费系统
我拿一个很常见的业务来演示:订单结算服务。要求如下:
- 普通用户:原价
- 会员:95 折
- VIP:8 折
- 企业客户:按协议价,可能是固定折扣,也可能是固定减免金额
- 后续还可能有:满减活动、限时折扣、内部测试价……
注意最后一条,这是关键。设计模式从来不是因为当前需求复杂才上,而是因为你已经能预见到“规则会继续变”才上。如果你确定十年不变,那写 if-else 也没毛病,不用为了模式而模式。
2.2 第一步:先定义策略接口
策略接口的设计是整个模式的灵魂。接口的方法参数怎么定、返回值怎么定,决定了后续每个策略长什么样。
public interface DiscountStrategy { /** * 计算最终支付金额 * * @param originalPrice 订单原始金额 * @param orderInfo 订单信息,可能包含用户等级、商品分类等上下文数据 * @return 优惠后的金额 */ double calculate(double originalPrice, OrderInfo orderInfo); }我建议参数不要只传一个 price,因为很多策略需要额外的上下文信息。比如企业客户可能要根据合同号查一下协议折扣;满减策略要根据订单里有没有指定品类来判断。所以传一个OrderInfo对象进去,扩展性会好很多。这是我在实际业务里用出来的经验:策略接口的方法参数尽量传一个上下文对象,而不是一堆零散参数,不然每次加规则都要改接口签名。
2.3 第二步:实现具体策略类
每个策略一个类,类名就是业务规则的名字,看了就知道它干什么。
public class NormalDiscount implements DiscountStrategy { @Override public double calculate(double originalPrice, OrderInfo orderInfo) { return originalPrice; } } public class MemberDiscount implements DiscountStrategy { @Override public double calculate(double originalPrice, OrderInfo orderInfo) { return originalPrice * 0.95; } } public class VipDiscount implements DiscountStrategy { @Override public double calculate(double originalPrice, OrderInfo orderInfo) { return originalPrice * 0.8; } } public class EnterpriseDiscount implements DiscountStrategy { @Override public double calculate(double originalPrice, OrderInfo orderInfo) { // 企业协议价:根据合同号查询折扣率 double ratio = queryContractRatio(orderInfo.getContractNo()); return originalPrice * ratio; } private double queryContractRatio(String contractNo) { // 伪代码:这里走RPC或者查本地配置表 return 0.75; } }你看,每个策略内部怎么做都是自己的事。EnterpriseDiscount里查合同、走 RPC,其他策略完全不知道,也不受影响。这就是“封装变化”的直观好处:你新增一个策略,只需要加一个类,不需要动其他任何已有类。
2.4 第三步:写上下文类并改造调用方
上下文类在简单场景下可以只是一个持有策略引用的工具类,复杂场景下还可以负责初始化默认策略、策略切换、记录日志等。
public class OrderCalculator { private DiscountStrategy strategy; public OrderCalculator(DiscountStrategy strategy) { this.strategy = strategy; } public void setStrategy(DiscountStrategy strategy) { this.strategy = strategy; } public double settle(double originalPrice, OrderInfo orderInfo) { double result = this.strategy.calculate(originalPrice, orderInfo); // 这里可以做日志、埋点、审计等横切操作 return result; } }客户端这样用:
OrderInfo order = new OrderInfo(); order.setUserLevel("vip"); order.setOriginalPrice(1000); DiscountStrategy strategy = StrategyFactory.getStrategy(order.getUserLevel()); OrderCalculator calculator = new OrderCalculator(strategy); double finalPrice = calculator.settle(order.getOriginalPrice(), order);改造完成之后,你原来的calculate方法变成了一组类,核心业务逻辑和算法细节分开了,后续加策略、改策略都变得可控。这一步走完,策略模式在项目里已经基本落地了。
2.5 高配版:简单工厂 + 策略,让调用方彻底解耦
上面的代码里,客户端还要自己判断userLevel对应哪个策略。这一步判断逻辑如果散落各处,还是会重复。更常见的做法是引入一个策略工厂:
public class StrategyFactory { private static final Map<String, DiscountStrategy> STRATEGY_MAP = new HashMap<>(); static { STRATEGY_MAP.put("normal", new NormalDiscount()); STRATEGY_MAP.put("member", new MemberDiscount()); STRATEGY_MAP.put("vip", new VipDiscount()); STRATEGY_MAP.put("enterprise", new EnterpriseDiscount()); } public static DiscountStrategy getStrategy(String userLevel) { DiscountStrategy strategy = STRATEGY_MAP.get(userLevel); if (strategy == null) { throw new IllegalArgumentException("No strategy for level: " + userLevel); } return strategy; } }客户端代码会变成:
OrderInfo order = buildOrderInfo(); double finalPrice = new OrderCalculator(StrategyFactory.getStrategy(order.getUserLevel())) .settle(order.getOriginalPrice(), order);这个组合非常经典:工厂负责“根据条件选策略”,策略对象负责“具体怎么做”,上下文负责“调用框架”。三者的责任边界非常清晰。我在真实项目中就用这个套路做过优惠券引擎,几十种券模板就是几十个策略类,工厂通过券类型编码去加载对应策略,新增券模板时业务代码一行都不用改。
3. Java 与 C++ 实现对照:套路相同,细节不同
既然热搜词里反复出现“设计模式 java 实现”和“C++ 23 种设计模式”,说明大家在看策略模式时,通常是在学两门语言。我在这里把 C++ 的常见写法也一起讲了。
3.1 C++ 经典写法:抽象基类 + 智能指针
C++ 里最正统的写法是用抽象基类定义策略接口,用std::unique_ptr或std::shared_ptr管理策略对象生命周期。
#include <iostream> #include <memory> class DiscountStrategy { public: virtual double calculate(double price) const = 0; virtual ~DiscountStrategy() = default; }; class NormalDiscount : public DiscountStrategy { public: double calculate(double price) const override { return price; } }; class MemberDiscount : public DiscountStrategy { public: double calculate(double price) const override { return price * 0.95; } }; class OrderCalculator { public: explicit OrderCalculator(std::unique_ptr<DiscountStrategy> strategy) : strategy_(std::move(strategy)) {} void setStrategy(std::unique_ptr<DiscountStrategy> strategy) { strategy_ = std::move(strategy); } double settle(double price) const { return strategy_->calculate(price); } private: std::unique_ptr<DiscountStrategy> strategy_; }; int main() { auto calc = OrderCalculator(std::make_unique<MemberDiscount>()); std::cout << calc.settle(1000) << std::endl; // 950 calc.setStrategy(std::make_unique<NormalDiscount>()); std::cout << calc.settle(1000) << std::endl; // 1000 return 0; }这里有个 C++ 特有的注意点:析构函数必须声明为 virtual。如果你忘了写虚析构,通过基类指针删除派生类对象时就是未定义行为,轻则内存泄漏,重则程序崩溃。这也是面试官最爱问的 C++ 策略模式陷阱。
3.2 C++ 现代写法:std::function 让策略“轻”起来
如果你的策略只是“一个函数”而不是“一族对象”,C++ 里完全可以用std::function实现更轻量的策略模式,连接口类和策略类都省了:
#include <iostream> #include <functional> class OrderCalculator { public: using Strategy = std::function<double(double)>; explicit OrderCalculator(Strategy strategy) : strategy_(std::move(strategy)) {} void setStrategy(Strategy strategy) { strategy_ = std::move(strategy); } double settle(double price) const { return strategy_(price); } private: Strategy strategy_; }; int main() { OrderCalculator calc([](double p) { return p * 0.95; }); std::cout << calc.settle(1000) << std::endl; // 950 calc.setStrategy([](double p) { return p * 0.8; }); std::cout << calc.settle(1000) << std::endl; // 800 return 0; }这也是我在实际 C++ 项目里用得最多的形式。当策略无状态、只关注算法本身时,lambda 表达式比定义一堆策略类高效得多,代码也更紧凑。但是要注意:如果策略本身有状态(比如要持有合同号、要连接外部服务),还是老老实实用类方案。
3.3 Java 中的函数式写法:Map + Lambda
Java 8 之后你也可以用函数接口 + Lambda 简化策略注册:
public class StrategyFactory { private static final Map<String, Function<OrderInfo, Double>> STRATEGY_MAP = new HashMap<>(); static { STRATEGY_MAP.put("normal", order -> order.getOriginalPrice()); STRATEGY_MAP.put("member", order -> order.getOriginalPrice() * 0.95); STRATEGY_MAP.put("vip", order -> order.getOriginalPrice() * 0.8); STRATEGY_MAP.put("enterprise", order -> { double ratio = queryContractRatio(order.getContractNo()); return order.getOriginalPrice() * ratio; }); } public static Function<OrderInfo, Double> getStrategy(String userLevel) { return STRATEGY_MAP.getOrDefault(userLevel, order -> { throw new IllegalArgumentException("unsupported level"); }); } }函数式写法适合策略逻辑非常简单的场景,一旦策略体超过十行或者要依赖其他服务,还是建议抽成完整的策略类,否则Map里的 Lambda 会变成一坨谁也看不懂的“代码集中营”。
4. 策略模式常见误用:到底该不该用、和谁搭配
4.1 策略模式 vs 工厂模式:两者不是替代关系
很多初学者会把策略模式和工厂模式搞混。其实它们的关注点完全不同:
- 工厂模式解决的是“对象怎么创建”的问题。调用方说“我要一个折扣策略”,工厂负责把对应的策略对象 new 出来。
- 策略模式解决的是“算法怎么替换”的问题。上下文说“我要用折扣策略计算出价格”,策略对象负责执行算法。
所以在实战里,工厂往往是策略模式的搭档,而不是对手。你完全可以用工厂来选择策略,再用策略模式来执行算法。这也是 2.5 节那个组合为什么踩着这么多年的坑还没过时。
4.2 策略模式 vs 状态模式:别看走眼
策略模式和状态模式的 UML 结构确实很像,都是上下文持有一个策略/状态对象,都是通过组合来改变行为。但它们解决的是完全不同的问题:
- 状态模式关注的是“状态内部转移”。状态对象自己决定下一个状态是谁,上下文的状态会随着业务推进自动切换。
- 策略模式关注的是“算法替换”。策略对象不关心下一个策略是谁,切换策略是由外部调用方或工厂决定的。
用一个例子区分:订单状态机(待支付、已支付、已发货、已完成)适合状态模式,因为状态之间有转移逻辑;订单计费适合策略模式,因为折扣算法之间没有依赖关系。判断标准很简单:如果你的对象会“自动变形”,那是状态模式;如果只是“换一个做法”,那是策略模式。
4.3 什么时候真的别用策略模式
策略模式虽好,但也别拿到什么都往里套。我见过最夸张的案例,是有人把一行a + b都封装成了策略类,一个项目新增十几个类文件。这是典型的过度设计。
下面几种情况不建议用策略模式:
- 算法基本不变:如果你的折扣规则一年到头就那两条,写 if-else 更直观,可维护性也不差。
- 策略类之间没有公共接口价值:如果两个“策略”的入参、出参都不同,硬凑到一个接口下,接口只会变得不伦不类。
- 团队维护水平不够:策略模式依赖多态,如果团队里有新手看不懂接口、组合这些概念,代码反而会更快腐化。
设计模式是工具,不是装饰品。当变化确实存在,且变化频率较高时,策略模式才真正有价值。
5. 实战中的问题排查与避坑经验
5.1 同一个策略对象要复用吗?单例还是每次新建?
策略对象一般是无状态的(除了一些配置数据),所以天然适合单例。我在项目里通常把策略注册到Map中一次性创建,后续就复用。这样省去了反复new的开销,也方便测试时替换。
但要注意:如果策略对象内部持有可变状态(比如计数、缓存),那就不能共享,否则多线程环境下会出大问题。我曾经遇到一个线上故障,就是因为策略类里加了一个实例变量记录调用次数,而策略对象在工厂里是单例的,导致并发环境下计数错乱,还影响到了后续订单的折扣计算。这件事之后,我定了一条规矩:无状态策略才允许单例,有状态策略必须每次新建。
5.2 策略类爆炸怎么办?用内部类、Lambda 和模板方法压缩
一个项目里几十个策略类确实会显得类很多。我常用的压缩方案有三种:
- 逻辑简单且只在一处使用:用 Lambda /
std::function替代独立类。 - 策略之间有大段公共逻辑:先抽一个抽象基类做模板方法,具体策略只覆盖差异部分。
- 策略按业务域聚合:比如把所有“营销活动折扣”策略放到一个包
promotion下,把所有“用户等级折扣”放到member下,别让策略类散落在项目各个角落。
命名也很重要。策略类名直接使用业务语言,如FirstOrderDiscount、FullReductionDiscount,不要叫什么StrategyA、StrategyB。好的命名能让半年后的你一眼就知道这个策略是干什么的。
5.3 策略切换的时机和上下文状态
有一个很隐蔽的坑:策略切换后,上下文中已有的临时状态可能不能沿用。比如你在OrderCalculator里缓存了上一次计算的明细,此时切换到新策略,缓存中的计算结果可能是用旧策略算出来的,如果直接复用就会出错。
我的建议是:策略切换之后,必须清理或重建上下文中的缓存字段。如果你不想每次切换都担心,可以考虑把策略对象设计成不可变对象,并把上下文中的缓存收敛到策略内部,而不是放在上下文里。这样策略一换,旧缓存自然作废。
5.4 测试策略模式时怎么 Mock?
策略模式的优点之一就是很好测。你不需要启动完整业务,只需要针对某个具体策略类写单测即可。
@Test void testMemberDiscount() { DiscountStrategy strategy = new MemberDiscount(); double result = strategy.calculate(1000, someOrderInfo()); assertEquals(950, result, 0.001); }如果上下文依赖了外部服务,比如EnterpriseDiscount要查合同,那就把查询逻辑抽象成接口,单测中注入一个 Mock 实现。记住:策略模式让每个算法的测试边界变得清晰,你要做的就是把外部依赖控制在策略内部的小接口上,别让策略直接依赖具体 RPC 或 DAO 实现。这样可以很方便地替换成测试替身。
5.5 大作业和面试里怎么讲出亮点?
如果你是在做设计模式大作业,或者准备面试,我建议你按这个顺序讲:
- 先抛出痛点:用一个 if-else 不断膨胀的真实例子,说明变化带来的维护成本。
- 再画三个角色:策略接口、具体策略、上下文,说清楚依赖方向。
- 现场写关键代码:不用写全部,写接口和上下文即可。
- 最后讲一个扩展案例:比如“如果我要新增一个企业协议折扣,只需要加一个策略类,注册到工厂,客户端一行不改”。
面试官其实不关心你会不会背定义,他关心的是你有没有在真实场景里做过取舍。这时候你补一句“我不用策略模式的情况是规则很少且不变”,比你背十遍“策略模式封装了一族算法”有用得多。
6. 策略模式的进阶场景:游戏开发与大型业务系统
热搜词里出现了“设计模式与游戏完美开发”,我顺便聊聊游戏开发里策略模式怎么用,这也是一个很容易出彩的场景。
6.1 游戏 AI 与角色行为策略
游戏里的角色 AI 是策略模式的经典舞台。比如一个怪物有三种攻击方式:近战、远程、范围毒爆。你在开发时就该定义IAttackStrategy接口,再分别实现MeleeAttack、RangedAttack、PoisonAttack。怪物根据玩家距离、血量、技能冷却等条件动态切换攻击策略:
public class Monster { private IAttackStrategy attackStrategy; public void update() { if (player.isFar() && skillOnCooldown) { setStrategy(new RangedAttack()); } else if (player.getHealth() < 20) { setStrategy(new PoisonAttack()); } attackStrategy.attack(player); } }相比写一长串if-else控制攻击逻辑,策略模式让每个行为变成了独立单元,游戏策划调数值、加新行为时,程序员不需要动核心战斗逻辑。这在大作业里绝对是降维打击级别的设计。
6.2 大型业务系统里的规则引擎
在电商、营销、金融这类后端系统里,策略模式最常被用来做规则引擎的骨架。比如优惠券系统,券模板分直减券、折扣券、满减券、叠加券,每一种券下去就是一个策略。你把“选券”逻辑交给工厂,“算券”逻辑交给策略,用户下单时再把选出的策略列表注入上下文执行。
这种设计的收益很直接:
- 新增一种券,只需新增一个策略类。
- 每个策略可以独立测试。
- 规则之间通过编排器组合,互不干扰。
我在实际项目里还加过一个“策略链”的概念:先顺序执行多个策略,每个策略决定自己是否命中,命中则改写订单金额。这就从策略模式自然过渡到了责任链模式。设计模式之间是可以组合的,这比单一模式更有威力。
6.3 稍作扩展:策略注册表与注解驱动
如果你不想每次加策略都改工厂,可以考虑用注册表模式 + 反射/注解。Java 里可以用EnumMap建立策略枚举与策略对象的映射:
public enum DiscountType { NORMAL(NormalDiscount.class), MEMBER(MemberDiscount.class), VIP(VipDiscount.class); public final Class<? extends DiscountStrategy> clazz; DiscountType(Class<? extends DiscountStrategy> clazz) { this.clazz = clazz; } }然后在 Spring 这类框架下,你甚至可以直接注入一个Map<String, DiscountStrategy>,Spring 会自动把容器中所有策略实现注册进来:
@Service public class StrategyRegister { private final Map<String, DiscountStrategy> strategyMap; public StrategyRegister(Map<String, DiscountStrategy> strategyMap) { this.strategyMap = strategyMap; } }这样新增策略类时只需要加@Component("enterprise"),工厂代码都不用动。当然,框架的魔法会掩盖一部分流程,新手看起来可能有点懵,所以我建议先用普通 Map 把原理吃透,再上注解驱动提升开发效率。
我在实际项目的体会是,策略模式最大的价值不是“消除 if-else”这么简单,而是它逼着你先想清楚“什么是稳定的,什么是会变的”,然后把会变的东西隔离起来。你写代码时每次觉得很痛苦、改动一处就牵连一片,那大概率就是变化没有被封装好的信号。策略模式不一定是你唯一的选择,但它绝对是一把称手的工具。如果你现在手上正好有一团乱麻的算法分支,不妨今天就试着抽出其中一个接口,再配上两个实现类,你会发现代码突然就顺眼了很多。