如果你在一个C++项目里待得够久,大概率会在某次代码评审时碰见一段层层嵌套的if-else。我最近一次碰到,是在一个订单计价模块里:普通用户、会员、节日促销、优惠券……每种规则都要在同一个函数里处理,而C++的策略模式,就是为了把这类“变化点”从业务逻辑里剥离开而存在的。这篇东西不打算从设计模式教科书的定义讲起,而是想聊聊我在真实项目里怎么用策略模式、怎么用现代C++把它写得比传统写法更轻,以及哪些场景下我反而会劝你别用。
1. 先从一段“变形的if-else”说起:策略模式到底在解决什么问题
1.1 一段越来越难维护的计价逻辑
假设你在写一个电商后端,最初的下单接口只有一种价格计算方式,代码大概长这样:
double calculatePrice(double basePrice, UserLevel level, double coupon) { double price = basePrice; if (level == UserLevel::Normal) { price = basePrice; } else if (level == UserLevel::Vip) { price = basePrice * 0.9; } else if (level == UserLevel::SuperVip) { price = basePrice * 0.8; } price -= coupon; return price; }半个月后产品经理说,节日全场八折,你加了一个if (isFestival)。又过了半个月,说新人首单立减50,你再塞一个if (isNewUser)。等到这个函数超过了一百行,你会发现自己已经不太敢改它了:每次上线都担心某条规则之间的顺序互相影响,测试用例多到跑一次要等半小时,新来的人接手时看这段代码的表情,基本可以用“地铁老人看手机”来形容。
1.2 问题的本质不是“分支多”,而是“变化点没有隔离”
把这段代码变乱的原因,并不是因为用了if-else。在C++里,if-else本身没有原罪。真正的问题在于:价格计算规则是一个典型的“变化点”,它几乎每周都在变,我却把它和“订单流程”这个相对稳定的部分硬绑在了一起。
变化的维度有三个:
- 新增规则时,我必须改动已有的
calculatePrice函数,这违背了开闭原则。开闭原则的意思是“对扩展开放,对修改关闭”,不过在多态支持良好的语言里,这句话更实际的理解是:新增一个行为时,新增一个类就够了,不需要回头改已经跑得好好的逻辑。 - 多条规则同时生效时,条件的排列组合会让逻辑爆炸。普通会员+节日+新人、普通会员+节日、超级会员+优惠券……每多一个维度,分支数量就往上涨一层。
- 测试成本越来越高。因为所有规则都揉在一个函数里,想单独验证“超级会员在节日时应该打几折”,你得构造上下文,把整个函数跑一遍。
这类问题的解法在面向对象领域已经很成熟:把“计算方式”这个会变化的点抽象成一个接口,每一种规则单独封装成类,业务上下文只依赖接口,不关心具体实现。这就是策略模式。
1.3 策略模式的角色划分,一句话就能说清
策略模式只有三个角色:
- 策略接口:定义“做什么”的抽象,比如
calculate(double amount)。 - 具体策略:各自实现一种“怎么做”,比如普通价、会员价、节日价。
- 上下文:持有一个策略对象,在需要的时候调用它,不关心策略内部是怎么算的。
用生活化的比喻,这就像你出门上班,交通方式就是策略接口,走路、骑车、坐地铁就是具体策略,而你本人是上下文。你只需要决定“今天用哪种策略”,至于每种交通方式怎么动、怎么等车,不需要在“你这个人”的代码里展开。
2. 经典三件套:策略基类、具体策略和上下文的落地写法
2.1 策略基类:只放一个纯虚函数就够了
先定义策略接口。以价格计算为例,我会写成这样:
class PriceStrategy { public: virtual ~PriceStrategy() = default; virtual double calculate(double basePrice) const = 0; };几个细节值得展开:
- 必须提供虚析构函数。因为上下文里往往持有一个
PriceStrategy*,而且大多数时候是一个std::unique_ptr<PriceStrategy>,如果基类析构函数不是虚的,删除派生类对象时就是未定义行为,具体表现为释放内存后可能崩溃、也可能不崩,但迟早会在某个诡异的地方等你。这个坑我在代码评审里见过不止一次。 - 纯虚函数建议加
const。策略对象在绝大多数场景下是无状态的,它只是一段“算法封装”,不修改自身成员。加上const之后,函数签名能更清楚地表达“计算这件事不会改变策略本身的内部状态”,也方便在函数参数里用const PriceStrategy&传递。 - 接口要足够窄。有的初学者喜欢在策略接口里同时放
init()、validate()、calculate()三个方法,理由是“策略对象可能需要初始化”。这会把接口做重,而且不同策略的初始化参数可能还不同,硬塞进接口只会让每个策略都实现一个永远不会被调用的空init()。我的建议是接口里只放调用方真正关心的那个方法,策略内部的初始化自己搞。
2.2 具体策略:一个类只封装一种规则
用一个具体的VipDiscountStrategy来演示:
class VipDiscountStrategy : public PriceStrategy { public: double calculate(double basePrice) const override { return basePrice * 0.9; } }; class FestivalDiscountStrategy : public PriceStrategy { public: explicit FestivalDiscountStrategy(double discountRate) : rate_(discountRate) {} double calculate(double basePrice) const override { return basePrice * rate_; } private: double rate_{0.8}; };FestivalDiscountStrategy展示了策略类的另一个重要特征:具体策略可以通过构造函数接收自己的参数。节日活动是全场八折还是七五折,不需要改类,而是通过构造函数传进来。这样同一个策略类可以应对不同的配置,避免为“八折”“七五折”各写一个类。
策略类不要持有与计算无关的成员。比如不要在里面放一个std::string userNote用来记录备注,那是订单对象该管的事。策略类越“干”,复用性和可测试性越高。
2.3 上下文:用组合替代继承
然后是实现价格计算的上下文类,也是业务代码主要打交道的地方:
class OrderPriceCalculator { public: explicit OrderPriceCalculator(std::unique_ptr<PriceStrategy> strategy) : strategy_(std::move(strategy)) {} double calculate(double basePrice) const { return strategy_->calculate(basePrice); } void setStrategy(std::unique_ptr<PriceStrategy> strategy) { strategy_ = std::move(strategy); } private: std::unique_ptr<PriceStrategy> strategy_; };这里的核心是组合:OrderPriceCalculator不自己实现价格算法,而是持有一个PriceStrategy对象。组合优于继承,因为价格策略的变化频率远高于订单计算器本身,如果把策略逻辑塞进OrderPriceCalculator的继承层次里,每多一种策略就要多一个子类,类数量会迅速膨胀。
运行时也可以换策略,这靠setStrategy实现。比如用户在结算页切换了会员等级,计算出当前该用哪个策略,然后动态换进去,OrderPriceCalculator不需要改任何代码。
2.4 一个可直接编译的完整示例
把上面的内容拼起来,就是一个完整可运行的示例:
#include <iostream> #include <memory> class PriceStrategy { public: virtual ~PriceStrategy() = default; virtual double calculate(double basePrice) const = 0; }; class NormalPriceStrategy : public PriceStrategy { public: double calculate(double basePrice) const override { return basePrice; } }; class VipDiscountStrategy : public PriceStrategy { public: double calculate(double basePrice) const override { return basePrice * 0.9; } }; class FestivalDiscountStrategy : public PriceStrategy { public: explicit FestivalDiscountStrategy(double rate) : rate_(rate) {} double calculate(double basePrice) const override { return basePrice * rate_; } private: double rate_; }; class OrderPriceCalculator { public: explicit OrderPriceCalculator(std::unique_ptr<PriceStrategy> strategy) : strategy_(std::move(strategy)) {} void setStrategy(std::unique_ptr<PriceStrategy> strategy) { strategy_ = std::move(strategy); } double calculate(double basePrice) const { return strategy_->calculate(basePrice); } private: std::unique_ptr<PriceStrategy> strategy_; }; int main() { OrderPriceCalculator calc(std::make_unique<NormalPriceStrategy>()); std::cout << "normal: " << calc.calculate(100.0) << "\n"; calc.setStrategy(std::make_unique<VipDiscountStrategy>()); std::cout << "vip: " << calc.calculate(100.0) << "\n"; calc.setStrategy(std::make_unique<FestivalDiscountStrategy>(0.8)); std::cout << "festival: " << calc.calculate(100.0) << "\n"; }运行结果:
normal: 100 vip: 90 festival: 80这就是策略模式的经典形态。回头再看开头的if-else,你会发现:新来一种价格规则,我只需要make_unique一个新的策略类,然后传给OrderPriceCalculator。之前的代码一行都不用动,这就是“对扩展开放,对修改关闭”的直观体现。
3. 现代C++给策略模式带来的变化:std::function、lambda与模板
3.1 不是所有策略都需要一个类
经典策略模式每新增一种规则就要新增一个类,代码文件数量会跟着涨。如果策略只有三五行的计算逻辑,这样确实有点重。现代C++给出了更轻的做法:用std::function把“策略”降级成一个可调用的值。
还是以价格计算为例,用std::function重写:
#include <functional> #include <unordered_map> #include <iostream> using PriceStrategyFn = std::function<double(double)>; class OrderPriceCalculator { public: explicit OrderPriceCalculator(PriceStrategyFn strategy) : strategy_(std::move(strategy)) {} void setStrategy(PriceStrategyFn strategy) { strategy_ = std::move(strategy); } double calculate(double basePrice) const { return strategy_(basePrice); } private: PriceStrategyFn strategy_; }; int main() { OrderPriceCalculator calc([](double price) { return price; }); std::cout << "normal: " << calc.calculate(100.0) << "\n"; calc.setStrategy([](double price) { return price * 0.9; }); std::cout << "vip: " << calc.calculate(100.0) << "\n"; // 捕获外部参数 double rate = 0.8; calc.setStrategy([rate](double price) { return price * rate; }); std::cout << "festival: " << calc.calculate(100.0) << "\n"; }std::function的优势在于:策略不再是一个“类型”,而是一个“值”。你可以把 lambda、函数指针、函数对象、甚至成员函数绑定(std::bind)塞进去,容器可以直接存std::function,这让策略的组合和传递变得异常灵活。
代价是性能:std::function内部通常会对可调用对象做一次类型擦除,涉及动态分配和间接调用,相比直接调用虚函数有额外开销。不过在实际业务系统里,这种开销基本可以忽略,除非你在做每帧调用百万次的性能敏感路径。如果你确实很在意,可以改用模板,下面会讲。
3.2 策略注册表:std::function让策略管理变得极其直观
std::function能放进std::unordered_map,这就为策略管理打开了一个很大的窗口:策略注册表。在经典写法里,要根据类型名字符串去找策略类,你通常还得写个工厂函数switch。现在可以这样:
#include <functional> #include <unordered_map> #include <string> #include <iostream> using PriceStrategyFn = std::function<double(double)>; class PriceStrategyRegistry { public: void registerStrategy(const std::string& name, PriceStrategyFn strategy) { strategies_[name] = std::move(strategy); } PriceStrategyFn get(const std::string& name) const { auto it = strategies_.find(name); if (it == strategies_.end()) { throw std::runtime_error("strategy not found: " + name); } return it->second; } private: std::unordered_map<std::string, PriceStrategyFn> strategies_; }; int main() { PriceStrategyRegistry registry; registry.registerStrategy("normal", [](double p) { return p; }); registry.registerStrategy("vip", [](double p) { return p * 0.9; }); registry.registerStrategy("festival", [](double p) { return p * 0.8; }); auto strategy = registry.get("vip"); std::cout << strategy(100.0) << "\n"; // 90 }这种注册表在业务系统里很常见。尤其是策略类型来自配置、来自前端参数时,一份Name -> 可调用对象的映射就能解决“运行时选择实现”的问题,不需要动用复杂的抽象工厂,也不需要维护一个巨大的switch。
3.3 模板策略与if constexpr:把策略选择提前到编译期
std::function和虚函数都是运行时多态,策略的选择发生在程序运行期间。但有时候,策略在编译期就已经完全确定了,比如一个程序同时支持 Windows 和 Linux,但某个特定平台上的算法是固定的。这时候用模板策略可以做到零开销抽象。
模板版本的策略选择,本质是让编译器在编译期确定调用哪个实现,形式上是把“策略”作为模板参数传给上下文:
template <typename Strategy> class OrderPriceCalculator { public: double calculate(double basePrice) const { return Strategy()(basePrice); } }; struct NormalPrice { double operator()(double price) const { return price; } }; struct VipPrice { double operator()(double price) const { return price * 0.9; } }; int main() { OrderPriceCalculator<NormalPrice> normalCalc; OrderPriceCalculator<VipPrice> vipCalc; std::cout << normalCalc.calculate(100.0) << "\n"; std::cout << vipCalc.calculate(100.0) << "\n"; }这里没有任何虚函数,也没有堆分配,调用在编译期就完全展开了。代价是你失去了运行时替换策略的能力:OrderPriceCalculator<VipPrice>和OrderPriceCalculator<NormalPrice>是两个完全不同的类型,存到一个容器里比较麻烦,通常需要用std::variant来容纳不同的策略对象。
C++17 引入的if constexpr还能让同一个模板函数在编译期“看到”不同的策略类型,并据此生成不同的代码。这在实现“一组算法在编译期选择不同实现路径”时非常有用。举个例子:
template <typename Policy> double calculateWithPolicy(double basePrice) { if constexpr (std::is_same_v<Policy, VipPrice>) { return basePrice * 0.9; } else { return basePrice; } }编译期策略的优势是性能上限高,劣势是灵活性低。实际工程里我的选择标准很简单:如果策略集合在编译期确定,并且性能要求极高,选模板;如果策略需要从配置或运行时输入动态决定,选std::function或虚函数。
3.4 三种策略承载方式怎么选
为了让你一眼看清,我把三种方式的核心差异整理成了表格:
| 方案 | 多态时机 | 运行时可替换 | 性能 | 代码量 | 使用场景 |
|---|---|---|---|---|---|
| 虚函数/派生类 | 运行时 | 支持 | 有虚表开销,但通常可忽略 | 较多 | 策略较多、需要扩展类层次、代码组织清晰 |
std::function/lambda | 运行时 | 支持 | 有类型擦除开销 | 少 | 策略逻辑短、希望轻量、需要加进容器 |
模板/if constexpr | 编译期 | 不支持 | 零开销 | 视场景 | 策略集合固定、性能敏感 |
我的个人倾向是:默认用std::function定义接口,如果策略本身有相对复杂的内部状态、需要多个方法协作完成计算,再用经典的虚函数类层次。模板策略只在非常明确的性能场景使用,因为它会把“策略的可替换性”牺牲掉,而策略模式原本强调的就是运行时灵活替换。
4. 策略对象从哪来:工厂、注册表与配置化装配
4.1 只写make_unique是远远不够的现实问题
经典示例里,调用方写calc.setStrategy(std::make_unique<VipDiscountStrategy>()),看起来简洁,但放在真实项目里这就成了问题:调用方不该知道“现在该用哪个具体策略”。这个决策通常应该由配置、用户等级或运行时的某个状态来决定。
如果这个决策逻辑散落在各个调用点,那策略模式的收益会大打折扣——你只是把 if-else 从calculatePrice挪到了调用方,并没有真正消灭它。所以实践中必须有一个地方集中产生策略对象,这就是工厂方法的任务。
4.2 工厂方法组装策略
一个最简单的策略工厂:
class PriceStrategyFactory { public: std::unique_ptr<PriceStrategy> create(const std::string& userLevel) const { if (userLevel == "vip") { return std::make_unique<VipDiscountStrategy>(); } if (userLevel == "super_vip") { return std::make_unique<SuperVipDiscountStrategy>(); } return std::make_unique<NormalPriceStrategy>(); } };工厂的好处是把“根据什么条件选什么策略”这层逻辑集中到一个类里。以后判断条件变了,比如从字符串变成了枚举、加上了用户积分维度,只改工厂内部,调用方完全无感。
这里有一个设计判断:如果条件判断只有两三个,直接写在工厂里没问题。如果条件的组合非常复杂(比如根据用户等级、订单金额、促销活动ID三个维度决定策略),那就得考虑链表式的职责链,或者直接上注册表。
4.3 基于字符串键的策略注册表
当策略数量膨胀到十几个,if-else工厂又开始难维护了。这时候可以和前面说的std::function注册表结合,做一套“注册式工厂”:
class PriceStrategyFactory { public: using StrategyCreator = std::function<std::unique_ptr<PriceStrategy>()>; void registerStrategy(const std::string& name, StrategyCreator creator) { creators_[name] = std::move(creator); } std::unique_ptr<PriceStrategy> create(const std::string& name) const { auto it = creators_.find(name); if (it == creators_.end()) { throw std::runtime_error("unknown strategy: " + name); } return it->second(); } private: std::unordered_map<std::string, StrategyCreator> creators_; };每个策略可以自己注册到工厂,比如在各策略类的实现文件里写一个静态注册变量。这样新增策略时,不需要改工厂类本体,只是在新的.cpp文件里注册自己,完全符合开闭原则。这种“自动注册”在大型C++项目里非常常见,尤其是插件化的架构里。
4.4 策略的生命周期管理:unique_ptr与shared_ptr的选择
策略对象的生命周期管理是工程上绕不开的问题。我见过不少策略模式的代码,策略对象被new出来之后,要么忘了释放,要么上下文被拷贝时出现双重释放。现代C++其实已经把这个问题简化了:默认使用std::unique_ptr表示独占所有权。
- 如果策略对象跟随上下文,由上下文创建或注入,上下文销毁时策略自动销毁,用
unique_ptr。 - 如果同一个策略对象被多个上下文共享,并且生命周期长于任何一个上下文,才考虑
std::shared_ptr。但共享策略对象最好是无状态的,否则多个上下文共用一份可变状态非常容易出诡异的bug。 - 如果策略本身是全局单例式的无状态对象,甚至可以不用智能指针,直接用
static局部变量的引用。比如getInstance()返回一个静态策略对象的引用,最适合NormalPriceStrategy这种永远不需要销毁的默认策略。
关于生命周期,我踩过的一个坑是:把策略对象以shared_ptr形式传进上下文,但上下文内部又调用了某个异步任务,任务捕获了策略的shared_ptr。结果策略对象被异步任务长期持有,上下文已经销毁了,异步任务还在拿策略算价格。这类问题往往不会立刻暴露,而是等到内存水位升高或性能劣化时才被发现。所以我的经验是:异步场景下,策略最好按值捕获,或者确保策略对象是不可变、无状态的。
5. 实战中容易踩的坑:状态模式混淆、粒度失控、过度设计
5.1 策略模式和状态模式,最容易混
策略模式看起来和状态模式很像,因为两者都是“把行为委托给另一个对象”。它们的核心区别在于切换行为的主导者是谁:
- 策略模式:外部主导切换。客户端明确说“我现在用VIP策略”,上下文被动接受,策略之间彼此独立,策略并不知道其他策略的存在。
- 状态模式:内部状态主导切换。对象内部维护一个当前状态,状态对象自己决定“下一个状态是什么”,上下文在运行时不断改变自己的行为。
举一个直观例子:一个网络连接对象有三种状态:连接中、已连接、已断开。已连接状态下收到数据可以正常处理,连接中状态下收到数据可能要缓冲起来。这里状态之间的流转往往是状态对象自己决定的,比如连接建立成功后,连接中状态会告诉上下文“切换到已连接状态”。这种“自己决定去向”的模型,就应该用状态模式而不是策略模式。
如果强行用策略模式实现状态流转,你会发现上下文里还是得写一堆判断“当前策略是否要切换到另一个策略”的逻辑,策略模式的封装优势反而被架空。所以动手前先问一句:切换这个行为,是外部代码给我的指令,还是被委托对象自己拥有的权利?前者用策略,后者用状态。
5.2 策略粒度:一个策略应该包含多少逻辑
策略粒度太粗的典型表现是:一个策略类里塞了“计算价格、校验库存、发送短信”三件事。粒度太细的表现则是:为了“折扣0.9”和“折扣0.8”各自写一个策略类,策略个数涨得比需求还快。
我的粒度标准是:一个策略类应该对应一个完整且独立的变化点,方法边界至少是“一个业务规则的整体”。比如“会员折扣”是一个完整变化点,哪怕VIP和超级VIP折扣率不一样,也可以共用一个MemberDiscountStrategy,通过构造参数传折扣率;而“新人立减”“节日折扣”是另一个维度的变化点,才拆成不同的策略。
判断粒度是否合适,可以问一个问题:**如果我要禁用某一个业务规则,我能不能只删掉一个策略类而不影响其他任何东西?**如果能,说明粒度基本合理;如果删掉一个类还要动别的类,说明你拆分时把耦合的字段或方法塞得太近了。
另外,如果策略类通过构造函数接收的参数超过三四个,说明这个策略可能承担了太多职责,可以考虑拆成多个策略,或者把参数封装成一个配置结构体。
5.3 什么时候不该用策略模式
策略模式不是银弹。下面这几种情况,我强烈建议你别硬上:
- 分支两个以内:只有一个
if-else,或者只有两种规则,直接写条件判断比引入策略模式清晰得多。我见过有人用一个接口加两个实现类来处理“开关”逻辑,打开、关闭各一个类,代码量翻了几倍,维护成本也上去了。这是典型的过度设计。 - 策略几乎不会变化:如果价格计算规则一年到头都不变,你花了大力气做的扩展点,根本没有需求来触发它。抽象没有变化点支撑,就是浪费。
- 策略内部严重共享状态:如果两种算法之间需要共享大量上下文数据,强行拆成策略类会导致策略构造参数很多、上下文和策略之间的传参乱成一团。这种情况下,先把“共享数据”和“变化行为”的关系理清楚再动手,而不是急着套模式。
5.4 多线程环境下使用策略的注意事项
搜索热词里有“c++多线程”,这里特意多说两句。策略模式本身不涉及并发,但在多线程项目里使用策略时,有几个坑很经典。
- 策略对象必须是线程安全的。如果策略内部有可变成员(比如缓存了上次计算的结果),多个线程同时调用同一个策略实例,就会产生数据竞争。最简单的解决办法是让策略对象无状态化:所有数据都通过方法参数传入,不在策略里保存中间结果。这不仅是线程安全的需要,也让策略类更容易测试。
- 策略基类最好明确标注线程安全性。在类注释里写清楚“这个类的策略实例是无状态的,可以被多个线程共享”或者“这个策略持有非线程安全缓存,使用方需要自行加锁”。这种注释不是可有可无,它能让后续维护者少踩很多坑。
- 不要在策略调用内部开启隐式全局锁。有些同学为了让策略线程安全,直接在每个策略方法里套
std::lock_guard<std::mutex>锁全局互斥量。这样做虽然能防止数据竞争,但会在并发量上来之后成为性能瓶颈。策略不应该负责全局同步,全局并发控制应该放在调用方或更上层。
6. 策略模式在大型C++项目里的组织方式与扩展思路
6.1 命名与文件组织:让策略自己会说话
策略类数量一多,命名的混乱程度会直接影响项目可维护性。我这里有一套实践规范,供参考:
- 基类命名用
Strategy结尾:DiscountStrategy、ExportStrategy、CompressionStrategy,一看名字就知道这是一个策略接口。 - 具体策略用“业务含义+Strategy”:
VipDiscountStrategy、FestivalDiscountStrategy,不要用StrategyA、StrategyB这种没有业务语义的命名。 - 每个策略一个文件,文件路径建议:
strategy/discount/vip_discount_strategy.h、strategy/discount/festival_discount_strategy.h。把同一类策略放在一个子目录里,便于做代码走查时快速定位。
命名清晰还有一个好处:有些人看代码历史记录时,能通过“哪个策略文件最近变动频繁”判断出这段时间产品在哪个方向上迭代。这个信息对判断模块的健康度非常有用。
6.2 策略的测试方法:为什么策略模式天然适合写单测
策略模式把每个算法隔离成了独立的类,这让单元测试变得非常清爽。为每个策略类写测试时,不需要构造整个订单流程,只需要构造一个最小输入,验证输出即可。
以FestivalDiscountStrategy为例:
TEST(FestivalDiscountStrategyTest, DiscountsRate) { FestivalDiscountStrategy strategy(0.8); EXPECT_DOUBLE_EQ(strategy.calculate(100.0), 80.0); }这种测试的好处是:一个策略的变更不会影响另一个策略的测试,测试失败时能立刻定位到是哪个策略出了问题。在大型项目里,这种“测试隔离性”的价值怎么强调都不过分。
如果你用std::function做策略载体,测试时甚至可以传入一个 mock lambda,验证上下文是否正确调用了策略、是否正确透传参数:
bool called = false; calc.setStrategy([&](double p) { called = true; return p + 1.0; }); calc.calculate(10.0); ASSERT_TRUE(called);6.3 策略模式与其它模式的常见搭配
在项目里,策略模式很少单独出现,它通常和下面几个模式配合。
- 工厂模式:这是最经典的组合。工厂负责根据运行条件创建合适的策略对象,策略模式负责执行算法。简单说,工厂解决“选哪个策略”,策略模式解决“怎么执行这个策略”。
- 模板方法模式:如果你有一组策略,它们的执行骨架完全一样,只是某个步骤不同,可以用模板方法定义骨架,策略模式定义那个变化步骤。比如报表导出策略,无论导出成 CSV 还是 JSON,都需要“打开文件、写入头部、写入数据、关闭文件”的流程,只有“写入数据的格式化方式”不同。这种情况下,骨架逻辑放在基类里,格式化步骤通过虚函数或注入的策略对象完成。
- 观察者模式:当策略执行后需要通知其他模块时,策略内部可以持有观察者列表。比如价格策略计算完成后,需要把优惠明细推送给审计系统。当然,为了让策略保持简单,我通常更倾向于把通知逻辑放在策略的调用方,而不是策略内部,只有在通知逻辑非常复杂且确实只属于这个策略时才放进策略里。
6.4 一个稍微进阶的实践:配置驱动的策略装配
最后分享一个我在项目中实际使用的进阶方案:结合配置文件+注册表,实现完全配置驱动的策略选择。
比如一份 JSON 配置:
{ "order_price_strategy": "vip", "festival_rate": 0.8 }程序启动时读取配置,把字符串"vip"传给注册表工厂,得到对应的策略对象。以后产品经理改了策略,不需要重新编译代码,只需要改配置。这种模式尤其适合需要快速调整业务规则的系统。
实现时需要注意一点:注册表里的策略创建函数必须能拿到配置信息。所以我的注册表往往是这样的:
using StrategyCreator = std::function<std::unique_ptr<PriceStrategy>(const ConfigItem&)>;ConfigItem里装着策略需要的参数,工厂创建策略时根据ConfigItem里的字段去构造。这样的好处是,策略的参数解析逻辑集中在一个地方,策略类本身不需要关心配置的格式。
最后再说一点个人体会。策略模式是我在 C++ 项目里用得最多的设计模式之一,但它不是越用越好。真正有价值的地方在于,它能逼着你在写代码之前先想清楚:这个模块里哪些部分是稳定的骨架,哪些部分是频繁变化的行为?把稳定的部分和变化的部分分开,这不是策略模式的专利,而是所有良好软件设计的共同原则。策略模式只是把这条原则落到了“行为变化”这一个维度上。如果你也遇到了一段不断膨胀的 if-else,不妨先做一步:把里面每个分支背后的“完整计算规则”列出来,看看它们是不是都能独立看待,如果是,那就可以把第一个策略类写出来了。