news 2026/9/9 10:49:21

C++策略模式实战:用现代C++轻量化解耦if-else

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++策略模式实战:用现代C++轻量化解耦if-else

如果你在一个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结尾DiscountStrategyExportStrategyCompressionStrategy,一看名字就知道这是一个策略接口。
  • 具体策略用“业务含义+Strategy”VipDiscountStrategyFestivalDiscountStrategy,不要用StrategyAStrategyB这种没有业务语义的命名。
  • 每个策略一个文件,文件路径建议:strategy/discount/vip_discount_strategy.hstrategy/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,不妨先做一步:把里面每个分支背后的“完整计算规则”列出来,看看它们是不是都能独立看待,如果是,那就可以把第一个策略类写出来了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 10:48:00

DeepSeek Harness:基座加插件架构的Agent开发工具链实战解析

这次我们来看一个近期在 Agent 开发圈子里讨论度上升很快的项目&#xff1a;DeepSeek Harness。它不是一个简单的模型封装&#xff0c;而是一套面向 Agent 场景的完整工具链&#xff0c;核心亮点有三个&#xff1a;基座性能对标 OpenAI Codex、插件系统异常灵活、部署上手路径清…

作者头像 李华
网站建设 2026/9/9 10:47:32

CMOS图像传感器动态范围工程化测量方法

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/9 10:46:40

MongoDB安装全指南:Windows、Linux与Docker环境实战与避坑

1. 安装 MongoDB 前&#xff0c;先把这三件事想清楚很多人装 MongoDB 失败&#xff0c;不是因为操作有多难&#xff0c;而是因为装之前压根没想清楚自己到底要装什么、装在哪里、怎么装。我在不同机器上折腾过太多次 MongoDB 了&#xff0c;从 Windows 笔记本到 Debian 服务器再…

作者头像 李华
网站建设 2026/9/9 10:45:34

DeepSeek Harness实战:从架构到插件开发,构建可扩展的Agent应用

最近在调研 Agent 开发框架时&#xff0c;我发现一个很典型的痛点&#xff1a;模型能力已经足够强&#xff0c;但真正把一个 Agent 从“能聊天”变成“能干活”&#xff0c;往往要花大量时间处理工具调用、插件接入、任务编排、状态管理这些底座问题。DeepSeek Harness 正是在这…

作者头像 李华
网站建设 2026/9/9 10:44:44

嵌入式黑盒协议逆向:从光耦时序建模到单片机插桩实战

1. 为什么“黑盒通信协议逆向”不是靠猜&#xff0c;而是靠拆解链路层级的系统工程嵌入式黑盒通信协议逆向——这八个字一出来&#xff0c;很多人第一反应是“拿示波器看波形、用逻辑分析仪抓包、IDA打开固件翻代码”&#xff0c;然后卡在第一步就停了。我2016年刚接手某国产工…

作者头像 李华
网站建设 2026/9/9 10:42:16

hermes-agent:多智能体协作的任务路由与消息分发中间层

如果你做过两个以上的 Agent 项目&#xff0c;大概会有一种感觉&#xff1a;模型变聪明了&#xff0c;但把多个模型拼在一起这件事&#xff0c;并没有变简单。最近我在梳理一套叫 hermes-agent 的多智能体调度层设计&#xff0c;它不做推理、不写 prompt&#xff0c;专门负责一…

作者头像 李华