C++策略模式实战:从电商折扣到算法替换的设计模式应用
1. 策略模式:从“硬编码”到“可插拔”的思维跃迁
在C++项目里摸爬滚打十几年,我见过太多因为算法或行为逻辑与业务主体代码“焊死”在一起,导致后期维护和扩展举步维艰的案例。比如,一个订单处理模块,最初只支持“普通用户折扣”,后来要加“VIP折扣”、“促销折扣”、“满减折扣”,代码里就开始出现大量的 if-else 或者 switch-case 分支。每加一种新策略,就要去修改核心的业务类,不仅违反了开闭原则,测试起来也让人头疼,生怕动了这里,那里又冒出个新bug。
这就是策略模式(Strategy Pattern)要解决的核心痛点。它不是什么高深莫测的黑科技,而是一种极其务实的设计思想: 将经常变化的算法或行为(策略)从稳定的上下文(Context)中剥离出来,封装成独立的、可互换的对象。 简单说,就是把“做什么”(策略)和“谁来做/在什么环境下做”(上下文)解耦。今天,我就结合自己踩过的坑和积累的经验,从里到外把C++策略模式给你掰扯清楚,让你不仅能看懂示例代码,更能理解何时用、怎么用、以及如何避开那些教科书里不会写的“坑”。
2. 策略模式的核心架构与工作原理
策略模式属于行为型设计模式,它的结构非常清晰,主要围绕三个角色展开: 策略接口(Strategy)、具体策略(ConcreteStrategy)和上下文(Context) 。理解它们之间的关系,是灵活运用这个模式的关键。
2.1 三大核心角色的职责解析
策略接口(Strategy) :这是一个抽象基类(在C++中通常是一个包含纯虚函数的类),它定义了所有具体策略必须实现的公共接口。这个接口就是策略执行任务的“契约”。例如,对于一个排序策略,接口可能是一个 sort(std::vector<int>&) 的纯虚函数;对于一个支付策略,接口可能是 pay(double amount) 。
注意 :策略接口的设计至关重要。它应该足够抽象,以涵盖所有潜在的具体策略行为,但又不能过于宽泛,导致具体策略实现时负担过重。通常,一个策略接口只定义一个核心的操作方法。
具体策略(ConcreteStrategy) :这些是策略接口的具体实现类。每个类都封装了一种独立的、完整的算法或行为。比如, QuickSortStrategy 和 BubbleSortStrategy 都实现了 SortStrategy 接口; CreditCardPayment 和 PayPalPayment 都实现了 PaymentStrategy 接口。它们是真正干活的“士兵”,但彼此独立,互不知晓。
上下文(Context) :上下文是使用策略的类。它并不关心当前使用的是哪种具体策略,它只认识策略接口。上下文通常通过组合(Composition)的方式持有一个指向策略接口的指针或引用(在现代C++中更推荐使用智能指针)。上下文将原本需要自己实现的算法逻辑,委托(Delegate)给当前持有的策略对象去执行。
2.2 策略模式的运作机制与数据流向
策略模式的工作流程,可以类比于将军(Context)指挥不同兵种(ConcreteStrategy)作战。将军不需要自己会开坦克或者驾驶飞机,他只需要有一个指挥所有兵种的通用命令(Strategy接口),然后根据战场情况,选择并派遣合适的兵种去执行任务。
- 初始化与装配 :客户端代码(Client)根据需求,创建某一个具体策略对象(如
ConcreteStrategyA),并将其传递给上下文对象(Context)。这个传递通常通过上下文的构造函数或一个专门的Setter方法(如setStrategy)完成。此时,上下文内部就持有了一个指向该策略对象的引用。 - 委托执行 :当上下文的某个业务方法(如
executeTask())被调用时,它不会自己处理核心逻辑,而是转而调用其所持有的策略对象的接口方法(如strategy_->doAlgorithm(data))。这就是“委托”。 - 动态替换 :如果业务需求发生变化,客户端可以在运行时动态地创建一个不同的具体策略对象(如
ConcreteStrategyB),并通过上下文的Setter方法替换掉旧的策略。上下文在接下来的操作中,就会自动使用新的策略,而自身的代码无需任何修改。
这种设计的精髓在于, 策略的变化被限制在了策略类的层次结构中,上下文因此变得稳定和可复用 。增加新的策略,只需要实现新的 ConcreteStrategyX 类,然后装配给上下文即可,完全符合“对扩展开放,对修改关闭”的开闭原则。
2.3 与相似模式的关键区别
新手常常混淆策略模式和状态模式(State Pattern),因为它们结构上非常相似,都是通过持有和委托一个接口对象来改变行为。
- 目的不同 :这是最本质的区别。 策略模式 是让客户端 主动选择 一种算法来完成某个任务。算法之间通常是平行的、可替代的,客户端清楚知道每个策略的差异并做出选择。例如,客户端主动选择用快速排序还是冒泡排序。
- 状态模式 则是对象 内部状态驱动 的行为改变。状态之间的转换通常由上下文对象自身或状态对象在运行时自动触发,客户端可能并不感知状态的存在。例如,一个TCP连接对象,其行为(建立连接、传输数据、断开连接)由它的状态(监听、已连接、已关闭)决定,状态的转换由网络事件触发。
简单说,策略是“你做这个事,我告诉你怎么做”;状态是“你现在是什么样,你就该怎么做”。
3. 策略模式的典型应用场景与实战选型
知道原理后,更关键的是知道什么时候该用它。策略模式不是银弹,滥用会增加不必要的抽象层次。下面这些场景,是策略模式大显身手的地方。
3.1 何时应该考虑引入策略模式
- 系统中有多种算法或行为,且它们经常需要互相替换 :这是最经典的场景。比如数据压缩(ZIP, RAR, 7z)、数据加密(AES, DES, RSA)、数据验证(邮箱验证、手机号验证、身份证验证)、渲染引擎(OpenGL, DirectX, Vulkan)等。这些算法职责清晰,接口相对统一。
- 需要避免使用多重条件选择语句(尤其是复杂的switch-case或if-else链) :当你发现一个类的方法体因为不同的条件分支而膨胀,并且这些分支对应的是不同的行为实现时,就应该考虑将每个分支提取为一个独立的策略类。
- 希望将算法的使用与其实现分离,以提高算法的独立性和复用性 :算法可以独立于上下文进行开发、测试和优化。同一个算法(如一个高效的排序策略)也可以被多个不同的上下文类使用。
- 需要在运行时动态切换算法 :比如一个图形编辑软件,用户可以在工具栏上实时切换不同的绘图工具(画笔、橡皮擦、填充桶),每种工具就是一个绘图策略。
3.2 一个完整的C++实战案例:电商促销引擎
让我们脱离简单的排序例子,看一个更贴近业务的场景:一个电商平台的促销折扣计算引擎。
假设我们有一个 Order (订单)类,最初它直接硬编码了折扣计算逻辑。随着业务发展,折扣规则越来越多,代码变得难以维护。我们使用策略模式进行重构。
首先,定义策略接口:
// DiscountStrategy.h
#ifndef DISCOUNT_STRATEGY_H
#define DISCOUNT_STRATEGY_H
#include <memory>
class DiscountStrategy {
public:
virtual ~DiscountStrategy() = default;
// 计算折扣后的价格
virtual double calculateDiscount(double originalPrice) const = 0;
// 获取策略描述(可选,用于日志或UI显示)
virtual std::string getStrategyName() const = 0;
};
#endif
接着,实现几种具体的折扣策略:
// ConcreteDiscountStrategies.h
#include “DiscountStrategy.h”
#include <cmath> // for std::ceil
// 无折扣策略
class NoDiscountStrategy : public DiscountStrategy {
public:
double calculateDiscount(double originalPrice) const override {
return originalPrice; // 原价返回
}
std::string getStrategyName() const override {
return “No Discount”;
}
};
// 百分比折扣策略
class PercentageDiscountStrategy : public DiscountStrategy {
private:
double percentage_; // 折扣率,如0.1代表9折
public:
explicit PercentageDiscountStrategy(double percentage) : percentage_(percentage) {
if (percentage < 0.0 || percentage >= 1.0) {
throw std::invalid_argument(“Discount percentage must be in [0, 1).”);
}
}
double calculateDiscount(double originalPrice) const override {
return originalPrice * (1.0 - percentage_);
}
std::string getStrategyName() const override {
return “Percentage Discount (“ + std::to_string(int(percentage_ * 100)) + “% Off)”;
}
};
// 满减折扣策略
class ThresholdDiscountStrategy : public DiscountStrategy {
private:
double threshold_; // 满减门槛
double reduction_; // 减免金额
public:
ThresholdDiscountStrategy(double threshold, double reduction)
: threshold_(threshold), reduction_(reduction) {
if (threshold <= 0 || reduction <= 0) {
throw std::invalid_argument(“Threshold and reduction must be positive.”);
}
}
double calculateDiscount(double originalPrice) const override {
if (originalPrice >= threshold_) {
return originalPrice - reduction_;
}
return originalPrice;
}
std::string getStrategyName() const override {
return “Threshold Discount (Over “ + std::to_string(threshold_) + “, Reduce “ + std::to_string(reduction_) + “)”;
}
};
然后,定义使用策略的上下文——订单类:
// Order.h
#include “DiscountStrategy.h”
#include <vector>
#include <string>
class Order {
private:
std::vector<std::pair<std::string, double>> items_; // 商品列表<名称,单价>
std::unique_ptr<DiscountStrategy> discountStrategy_; // 持有的折扣策略
public:
// 构造函数,默认无折扣
Order() : discountStrategy_(std::make_unique<NoDiscountStrategy>()) {}
// 设置折扣策略
void setDiscountStrategy(std::unique_ptr<DiscountStrategy> strategy) {
if (strategy) {
discountStrategy_ = std::move(strategy);
}
}
void addItem(const std::string& name, double price) {
items_.emplace_back(name, price);
}
// 计算订单总价(应用折扣)
double calculateTotal() const {
double subtotal = 0.0;
for (const auto& item : items_) {
subtotal += item.second;
}
// 委托给策略对象计算最终价格
return discountStrategy_->calculateDiscount(subtotal);
}
// 获取当前使用的策略名称
std::string getCurrentDiscountInfo() const {
return discountStrategy_->getStrategyName();
}
// ... 其他订单相关方法
};
最后,客户端代码可以灵活地组合和使用:
// main.cpp
#include “Order.h”
#include “ConcreteDiscountStrategies.h”
#include <iostream>
int main() {
Order order;
order.addItem(“C++ Primer”, 99.99);
order.addItem(“Design Patterns Book”, 89.99);
std::cout << “Initial total (No discount): $” << order.calculateTotal() << std::endl;
std::cout << “Strategy: “ << order.getCurrentDiscountInfo() << “\n\n”;
// 动态切换为9折策略
order.setDiscountStrategy(std::make_unique<PercentageDiscountStrategy>(0.1)); // 10% off
std::cout << “After 10% discount: $” << order.calculateTotal() << std::endl;
std::cout << “Strategy: “ << order.getCurrentDiscountInfo() << “\n\n”;
// 动态切换为满150减30策略
order.setDiscountStrategy(std::make_unique<ThresholdDiscountStrategy>(150.0, 30.0));
// 订单总额 99.99+89.99=189.98 > 150,触发满减
std::cout << “After ‘Over 150 reduce 30’ discount: $” << order.calculateTotal() << std::endl;
std::cout << “Strategy: “ << order.getCurrentDiscountInfo() << std::endl;
return 0;
}
这个案例清晰地展示了策略模式如何让折扣逻辑变得可插拔、易扩展。如果要新增一个“第二件半价”的策略,你只需要创建一个新的 SecondHalfPriceStrategy 类,实现 DiscountStrategy 接口,然后在客户端调用 setDiscountStrategy 即可, Order 类一行代码都不用改。
4. 策略模式的多种实现方法与进阶技巧
掌握了基础用法后,我们来看看在C++中实现策略模式的一些变体和进阶技巧,这些能让你在实际项目中用得更顺手。
4.1 策略对象的生命周期管理
这是C++实现中需要特别注意的一点。上下文(Context)持有策略对象的指针或引用,那么策略对象由谁创建,由谁销毁?
- 传统裸指针(不推荐) :上下文不负责策略对象的生命周期,由客户端创建和销毁。这容易导致内存泄漏或悬空指针。
// 不推荐的做法 Context ctx(new ConcreteStrategyA()); // 需要记得delete - 智能指针(推荐) :使用
std::unique_ptr或std::shared_ptr来自动管理内存。这是现代C++的最佳实践。-
std::unique_ptr:表示上下文独占策略对象的所有权。策略对象随上下文的销毁而销毁,或者通过setStrategy被替换时自动销毁。上面的订单例子就采用了这种方式。它简单、高效,适用于大多数策略对象无需共享的场景。 -
std::shared_ptr:当同一个策略对象可能需要被多个上下文共享时使用(这种情况相对较少)。需要小心循环引用问题。
-
- 值语义与
std::function(C++11及以上) :如果策略非常轻量(例如只是一个简单的比较函数),并且不需要复杂的多态,可以考虑不使用继承层次。C++的泛型和函数对象提供了更灵活的方案。
这种方式更加灵活和轻量,避免了虚函数调用的开销,适合策略逻辑简单、且变化点不多的场景。#include <functional> #include <vector> #include <algorithm> class Sorter { private: // 使用std::function作为策略类型 std::function<bool(int, int)> comparisonStrategy_; public: // 默认策略为升序 Sorter() : comparisonStrategy_([](int a, int b) { return a < b; }) {} void setStrategy(std::function<bool(int, int)> strategy) { comparisonStrategy_ = strategy; } void sortVector(std::vector<int>& vec) { std::sort(vec.begin(), vec.end(), comparisonStrategy_); } }; // 客户端使用 Sorter sorter; std::vector<int> data = {5, 2, 8, 1}; sorter.sortVector(data); // 升序排序 // 动态切换为降序策略 sorter.setStrategy([](int a, int b) { return a > b; }); sorter.sortVector(data); // 降序排序
4.2 策略的创建与工厂模式结合
当具体策略类很多,且它们的创建逻辑复杂(例如需要从配置文件中读取参数来构造)时,直接在客户端代码里 new 具体的策略对象会使得客户端代码与具体策略类耦合,并且创建逻辑分散。
这时,可以引入 工厂模式(Factory Pattern) 或 简单工厂 来封装策略对象的创建过程。
// DiscountStrategyFactory.h
#include “ConcreteDiscountStrategies.h”
#include <string>
#include <memory>
class DiscountStrategyFactory {
public:
static std::unique_ptr<DiscountStrategy> createStrategy(const std::string& type, const std::string& params) {
if (type == “percentage”) {
// 假设params格式为 “10” 表示10%
double pct = std::stod(params) / 100.0;
return std::make_unique<PercentageDiscountStrategy>(pct);
} else if (type == “threshold”) {
// 假设params格式为 “150,30”
size_t pos = params.find(‘,’);
double th = std::stod(params.substr(0, pos));
double re = std::stod(params.substr(pos + 1));
return std::make_unique<ThresholdDiscountStrategy>(th, re);
} else if (type == “none”) {
return std::make_unique<NoDiscountStrategy>();
}
throw std::invalid_argument(“Unknown discount strategy type: “ + type);
}
};
// 客户端使用
// 从配置读取 type=“percentage”, params=“15”
auto strategy = DiscountStrategyFactory::createStrategy(config.type, config.params);
order.setDiscountStrategy(std::move(strategy));
这样,客户端代码只需要和工厂类打交道,完全不需要知道 PercentageDiscountStrategy 等具体类的存在,进一步降低了耦合。
4.3 策略模式与模板的结合(编译期策略)
对于性能要求极其苛刻的场景,虚函数调用(动态多态)带来的运行时开销可能是不可接受的。C++的模板(静态多态)可以在编译期绑定策略,完全消除运行时开销。
// 策略作为模板参数
template <typename SortingStrategy>
class SorterContext {
private:
SortingStrategy strategy_;
public:
// 上下文可以直接使用策略对象,无需通过指针或引用
void sortData(std::vector<int>& data) {
strategy_.sort(data); // 假设SortingStrategy有sort方法
}
};
// 具体策略作为普通类(无需继承自统一接口)
class QuickSortStrategy {
public:
void sort(std::vector<int>& data) const {
// 实现快速排序
std::sort(data.begin(), data.end()); // 简单示意
}
};
class BubbleSortStrategy {
public:
void sort(std::vector<int>& data) const {
// 实现冒泡排序
for (size_t i = 0; i < data.size(); ++i) {
for (size_t j = 0; j < data.size() - i - 1; ++j) {
if (data[j] > data[j + 1]) {
std::swap(data[j], data[j + 1]);
}
}
}
}
};
// 客户端使用
std::vector<int> myData = {…};
SorterContext<QuickSortStrategy> sorter1;
sorter1.sortData(myData); // 使用快速排序
SorterContext<BubbleSortStrategy> sorter2;
sorter2.sortData(myData); // 使用冒泡排序
注意 :模板方法的缺点是策略必须在编译时确定,无法在运行时动态切换。它和基于继承的动态策略模式是互补的,适用于不同的场景。
5. 实践中常见的“坑”与解决方案
即使理解了原理,在实际项目中应用策略模式时,还是会遇到一些教科书上没细说的问题。下面是我总结的几个常见坑点和解决思路。
5.1 策略对象需要访问上下文数据怎么办?
有时,策略的执行不仅仅依赖于传入的参数,还需要知道上下文(Context)自身的一些状态信息。例如,一个“运费计算策略”可能需要知道订单的重量、收货地址(属于订单上下文的信息)。
不好的做法 :在策略接口的方法中传入整个上下文对象的引用。这会导致策略与上下文紧密耦合,策略可能滥用上下文的其他方法,破坏封装性。
推荐做法 :只传递策略执行所需的最小数据集。修改策略接口,使其接收必要的参数。
// 不好的设计:传递整个上下文
class ShippingStrategy {
public:
virtual double calculate(const OrderContext& context) = 0; // 耦合度高
};
// 好的设计:传递所需的最小数据
class ShippingStrategy {
public:
virtual double calculate(double weightKg, const std::string& destinationZipCode) = 0;
};
// 上下文中的调用
double Order::calculateShipping() {
return shippingStrategy_->calculate(this->getTotalWeight(), this->getDeliveryZipCode());
}
如果所需的数据很多,可以考虑将这些数据封装成一个 参数对象(Parameter Object) ,比如 ShippingInfo ,包含重量、邮编、包裹尺寸等。这样既保持了接口简洁,又避免了策略与上下文的过度耦合。
5.2 策略类爆炸与组合使用
如果一个系统有大量的、细粒度的策略,可能会导致策略类的数量急剧增加(类爆炸)。例如,一个游戏角色有10种攻击方式和10种防御方式,如果为每种组合都创建一个策略类,那就是100个类。
解决方案 :考虑使用 组合模式(Composite Pattern) 或者将策略本身设计得更通用。
- 组合策略 :创建一个“组合策略”,它内部包含多个子策略,并按特定顺序或逻辑执行它们。例如,一个“复合折扣策略”可以先应用满减,再应用百分比折扣。
class CompositeDiscountStrategy : public DiscountStrategy { private: std::vector<std::unique_ptr<DiscountStrategy>> strategies_; public: void addStrategy(std::unique_ptr<DiscountStrategy> strategy) { strategies_.push_back(std::move(strategy)); } double calculateDiscount(double originalPrice) const override { double currentPrice = originalPrice; for (const auto& strategy : strategies_) { currentPrice = strategy->calculateDiscount(currentPrice); } return currentPrice; } // … getStrategyName }; - 参数化策略 :与其为细微的差异创建新类,不如让一个策略类通过构造函数参数或配置来调整其行为。例如,一个
FixedAmountDiscountStrategy,折扣金额通过参数传入,而不是为“减5元”、“减10元”分别创建类。
5.3 性能考量:虚函数开销与对象创建
在性能敏感的系统中(如高频交易、游戏渲染循环),需要关注两点:
- 虚函数调用开销 :动态多态通过虚函数表(vtable)实现,每次调用都有一次间接寻址的开销。对于在紧密循环中每秒调用数百万次的策略,这个开销可能需要评估。
- 对策 :如果策略在运行时不变,可以考虑使用上面提到的模板方法(静态多态)。或者,如果策略非常简单,可以尝试用
std::function或函数指针,有时编译器优化效果更好。
- 对策 :如果策略在运行时不变,可以考虑使用上面提到的模板方法(静态多态)。或者,如果策略非常简单,可以尝试用
- 策略对象的创建与销毁开销 :频繁地动态创建和销毁策略对象(尤其是在运行时切换策略时)可能带来内存分配开销。
- 对策 :如果策略类型有限且无状态(即不包含成员变量,或成员变量在每次使用前重置),可以考虑使用 享元模式(Flyweight Pattern) ,复用策略对象实例。或者,使用对象池(Object Pool)来管理策略对象的生命周期。
5.4 如何优雅地处理“默认策略”或“空策略”?
上下文在策略未被设置时应该怎么办?直接崩溃或返回错误值不友好。
解决方案 :提供一个“空对象(Null Object)策略”。这是一个实现了策略接口,但方法体为空或执行默认无害行为的类。
class NullDiscountStrategy : public DiscountStrategy {
public:
double calculateDiscount(double originalPrice) const override {
return originalPrice; // 什么都不做,返回原价
}
std::string getStrategyName() const override {
return “Null Strategy (No Operation)”;
}
};
// 在上下文构造函数中初始化一个空策略
Order::Order() : discountStrategy_(std::make_unique<NullDiscountStrategy>()) {}
这样,即使客户端忘记设置策略,系统也能以一种定义良好的、安全的方式继续运行,避免了空指针异常。
6. 从策略模式看优秀软件设计原则
策略模式不仅仅是一个模式,它更是多种经典设计原则的生动体现。理解这些原则,能让你在更宏观的层面把握设计方向。
- 开闭原则(Open-Closed Principle, OCP) :这是策略模式最直接体现的原则。上下文对修改是封闭的(不需要修改代码就能适应新策略),而策略层次对扩展是开放的(可以随意增加新的
ConcreteStrategy)。 - 单一职责原则(Single Responsibility Principle, SRP) :策略模式将不同的算法或行为分离到不同的策略类中。每个策略类只负责一个具体的算法实现,上下文类只负责维护策略和使用策略,各自的职责变得清晰单一。
- 依赖倒置原则(Dependency Inversion Principle, DIP) :上下文(高层模块)不再依赖具体策略(低层模块),而是共同依赖于抽象的
Strategy接口。这降低了模块间的耦合度。 - 组合优于继承(Composition over Inheritance) :策略模式通过组合(在Context中持有Strategy)来获得行为的灵活性,而不是通过继承上下文类并重写其方法。这提供了更大的弹性,因为行为可以在运行时被改变,而且可以混合使用多个不同策略家族的行为。
在实际架构评审中,当你看到一大片条件分支语句处理各种业务变体时,就可以思考:“这里是否可以用策略模式来重构?” 这往往是将代码从“能工作”推向“好维护”的关键一步。
策略模式是一个入门简单但内涵丰富的模式。它教会我们的,是一种分解复杂问题、隔离变化点的思维方式。在C++中实现它,需要仔细考虑对象生命周期、性能开销和接口设计。当你下次在代码中写下又一个 if-else 时,不妨停一下,想想这个逻辑未来会不会变,如果会,那么策略模式可能就是你的最佳拍档。记住,好的设计不是一次性写出最完美的代码,而是让代码在面对未来的变化时,依然能够从容、优雅地应对。
更多推荐



所有评论(0)