1. 策略模式:何时该用这个设计模式?
在C++开发中,我们经常会遇到这样的场景:一个算法或行为需要在运行时根据条件动态切换。比如支付系统需要支持多种支付方式(信用卡、支付宝、微信),游戏角色需要根据装备切换不同的攻击策略,或者电商系统需要根据促销活动调整价格计算逻辑。
策略模式(Strategy Pattern)正是为解决这类问题而生的。它定义了一系列算法,将每个算法封装起来,并使它们可以相互替换。策略模式让算法的变化独立于使用算法的客户端。
提示:策略模式特别适合以下场景:
- 一个系统需要在多种算法中选择一种
- 需要避免使用多重条件判断语句
- 算法需要自由切换或扩展
2. 策略模式的C++实现详解
2.1 基础类结构设计
策略模式的核心在于将算法抽象为接口,具体实现由派生类完成。典型的C++实现包含三个部分:
- 策略接口(Strategy):定义所有支持的算法的公共接口
- 具体策略(ConcreteStrategy):实现策略接口的具体算法类
- 上下文(Context):持有一个策略对象的引用,通过策略接口与具体策略交互
cpp复制// 策略接口
class SortingStrategy {
public:
virtual void sort(std::vector<int>& data) = 0;
virtual ~SortingStrategy() = default;
};
// 具体策略:快速排序
class QuickSort : public SortingStrategy {
public:
void sort(std::vector<int>& data) override {
std::cout << "Sorting using QuickSort" << std::endl;
// 实际快速排序实现...
}
};
// 具体策略:归并排序
class MergeSort : public SortingStrategy {
public:
void sort(std::vector<int>& data) override {
std::cout << "Sorting using MergeSort" << std::endl;
// 实际归并排序实现...
}
};
// 上下文
class Sorter {
SortingStrategy* strategy;
public:
Sorter(SortingStrategy* strategy) : strategy(strategy) {}
void setStrategy(SortingStrategy* newStrategy) {
strategy = newStrategy;
}
void executeSort(std::vector<int>& data) {
strategy->sort(data);
}
};
2.2 现代C++的改进实现
C++11及以后版本提供了更多现代特性,我们可以改进传统实现:
cpp复制// 使用std::function和lambda
class ModernSorter {
std::function<void(std::vector<int>&)> strategy;
public:
explicit ModernSorter(std::function<void(std::vector<int>&)> strat)
: strategy(strat) {}
void setStrategy(std::function<void(std::vector<int>&)> newStrat) {
strategy = newStrat;
}
void execute(std::vector<int>& data) {
strategy(data);
}
};
// 使用示例
ModernSorter sorter([](std::vector<int>& data) {
std::sort(data.begin(), data.end());
});
这种实现更灵活,可以直接使用lambda表达式作为策略,减少了类的定义。
3. 实战案例:电商运费计算系统
让我们通过一个电商运费计算的完整案例来展示策略模式的实际应用。根据热词中提到的需求:"根据快递的件数和用户是否选择加急计算快递费"。
3.1 需求分析
计算规则:
- 件数在10件以内:基础运费15元
- 超过10件:每增加1件加收2元
- 加急服务:总运费×1.5
- 大客户折扣:总运费×0.8(可选)
3.2 策略模式实现
cpp复制// 运费计算策略接口
class ShippingStrategy {
public:
virtual double calculate(const Order& order) = 0;
virtual ~ShippingStrategy() = default;
};
// 普通运费策略
class StandardShipping : public ShippingStrategy {
public:
double calculate(const Order& order) override {
double base = order.itemCount <= 10 ? 15 : 15 + (order.itemCount - 10) * 2;
return order.isExpress ? base * 1.5 : base;
}
};
// 大客户运费策略
class VIPShipping : public ShippingStrategy {
public:
double calculate(const Order& order) override {
double base = order.itemCount <= 10 ? 15 : 15 + (order.itemCount - 10) * 2;
base = order.isExpress ? base * 1.5 : base;
return base * 0.8; // VIP折扣
}
};
// 订单类
struct Order {
int itemCount;
bool isExpress;
bool isVIP;
};
// 运费计算上下文
class ShippingCalculator {
std::unique_ptr<ShippingStrategy> strategy;
public:
explicit ShippingCalculator(std::unique_ptr<ShippingStrategy> strat)
: strategy(std::move(strat)) {}
void setStrategy(std::unique_ptr<ShippingStrategy> newStrat) {
strategy = std::move(newStrat);
}
double calculateShipping(const Order& order) {
return strategy->calculate(order);
}
};
// 使用示例
Order order{12, true, false};
ShippingCalculator calculator(std::make_unique<StandardShipping>());
double cost = calculator.calculateShipping(order);
if (order.isVIP) {
calculator.setStrategy(std::make_unique<VIPShipping>());
cost = calculator.calculateShipping(order);
}
3.3 策略工厂模式扩展
为了更方便地创建策略对象,可以引入工厂模式:
cpp复制class ShippingStrategyFactory {
public:
static std::unique_ptr<ShippingStrategy> create(bool isVIP) {
return isVIP ? std::make_unique<VIPShipping>()
: std::make_unique<StandardShipping>();
}
};
// 使用工厂简化客户端代码
auto strategy = ShippingStrategyFactory::create(order.isVIP);
ShippingCalculator calculator(std::move(strategy));
4. 策略模式的高级应用与优化
4.1 策略模式与模板方法的结合
策略模式可以与模板方法模式结合,在保持算法结构的同时允许某些步骤变化:
cpp复制class CompressionStrategy {
public:
void compress(const std::string& file) {
preProcess(file);
doCompress(file);
postProcess(file);
}
virtual ~CompressionStrategy() = default;
protected:
virtual void doCompress(const std::string& file) = 0;
void preProcess(const std::string& file) {
std::cout << "Preprocessing " << file << std::endl;
}
void postProcess(const std::string& file) {
std::cout << "Postprocessing " << file << std::endl;
}
};
class ZipCompression : public CompressionStrategy {
protected:
void doCompress(const std::string& file) override {
std::cout << "Compressing " << file << " using ZIP" << std::endl;
}
};
class RarCompression : public CompressionStrategy {
protected:
void doCompress(const std::string& file) override {
std::cout << "Compressing " << file << " using RAR" << std::endl;
}
};
4.2 使用策略模式优化条件分支
策略模式可以有效替代复杂的条件分支语句。考虑一个游戏角色攻击方式的例子:
cpp复制// 不好的实现:使用条件分支
void attack(const Character& character) {
switch(character.getWeaponType()) {
case SWORD:
swordAttack();
break;
case BOW:
bowAttack();
break;
case STAFF:
staffAttack();
break;
// 每增加一种武器类型就需要修改这里
}
}
// 好的实现:使用策略模式
class AttackStrategy {
public:
virtual void attack() = 0;
virtual ~AttackStrategy() = default;
};
class Character {
std::unique_ptr<AttackStrategy> strategy;
public:
void setAttackStrategy(std::unique_ptr<AttackStrategy> newStrategy) {
strategy = std::move(newStrategy);
}
void attack() {
strategy->attack();
}
};
4.3 策略对象的生命周期管理
在C++中,策略对象的生命周期管理需要特别注意:
- 使用智能指针:推荐使用std::unique_ptr或std::shared_ptr管理策略对象
- 策略无状态时的优化:如果策略无状态,可以单例化或静态化
- 策略的线程安全性:确保策略对象在多线程环境下的安全使用
cpp复制// 无状态策略的单例实现
class LogStrategy {
public:
virtual void log(const std::string& message) = 0;
virtual ~LogStrategy() = default;
};
class ConsoleLogger : public LogStrategy {
ConsoleLogger() = default;
public:
static ConsoleLogger& instance() {
static ConsoleLogger logger;
return logger;
}
void log(const std::string& message) override {
std::cout << message << std::endl;
}
};
// 使用示例
Logger logger(&ConsoleLogger::instance());
logger.log("Test message");
5. 策略模式的优缺点与适用场景
5.1 策略模式的优势
- 开闭原则:可以在不修改客户端代码的情况下引入新策略
- 消除条件语句:避免了大量的if-else或switch-case语句
- 算法复用:不同的上下文可以共享同一个策略对象
- 运行时灵活性:算法可以在运行时动态切换
5.2 策略模式的局限性
- 客户端必须了解策略:客户端需要知道不同策略的区别以选择合适的策略
- 对象数量增加:每个策略都是一个类,可能增加系统中类的数量
- 通信开销:策略与上下文之间可能需要交换数据,增加通信开销
5.3 何时使用策略模式
策略模式特别适用于以下场景:
- 一个系统需要在多种算法中选择一种
- 需要避免暴露复杂的、与算法相关的数据结构
- 算法需要自由切换或扩展
- 有多个条件语句的相同行为
我在实际项目中使用策略模式的一个经验是:当发现自己在写第三个相似的if-else分支时,就应该考虑是否可以用策略模式重构了。这不仅使代码更清晰,也为未来的扩展预留了空间。
6. 策略模式与其他设计模式的关系
6.1 策略模式与状态模式
策略模式和状态模式在结构上相似,但意图不同:
- 策略模式:客户端主动选择算法
- 状态模式:状态转换由内部条件决定,客户端不直接指定
6.2 策略模式与命令模式
两者都用于封装行为,但:
- 策略模式:关注算法的替换
- 命令模式:关注请求的封装和排队
6.3 策略模式与模板方法模式
模板方法模式在类层次上定义算法骨架,而策略模式通过对象组合来切换整个算法。
7. 实际项目中的经验分享
7.1 性能考量
在性能敏感的场景中,策略模式的虚函数调用开销可能成为瓶颈。可以考虑以下优化:
- 策略对象池:重用无状态策略对象
- CRTP模式:使用静态多态替代动态多态
- 策略内联:对于简单策略,直接内联实现
cpp复制// 使用CRTP实现静态策略模式
template<typename T>
class SortingStrategy {
public:
void sort(std::vector<int>& data) {
static_cast<T*>(this)->sortImpl(data);
}
};
class QuickSort : public SortingStrategy<QuickSort> {
public:
void sortImpl(std::vector<int>& data) {
// 快速排序实现
}
};
template<typename Strategy>
class Sorter {
Strategy strategy;
public:
void execute(std::vector<int>& data) {
strategy.sort(data);
}
};
// 使用示例
Sorter<QuickSort> sorter;
sorter.execute(data);
7.2 测试策略
策略模式的一个巨大优势是便于单元测试。可以为每个策略编写独立的测试用例,甚至可以使用模拟策略来测试上下文行为。
cpp复制// 测试用模拟策略
class MockShippingStrategy : public ShippingStrategy {
public:
double calculate(const Order& order) override {
return 42.0; // 固定返回值便于测试
}
};
TEST(ShippingCalculatorTest, CalculateTest) {
Order testOrder{5, false, false};
ShippingCalculator calculator(std::make_unique<MockShippingStrategy>());
ASSERT_EQ(42.0, calculator.calculateShipping(testOrder));
}
7.3 设计陷阱与规避
- 过度设计:对于永远不会变化的算法,直接实现可能更简单
- 策略膨胀:当策略类过多时,考虑使用其他模式(如责任链)
- 上下文臃肿:避免让上下文知道太多策略细节
我在一个电商项目中曾经过度使用策略模式,导致系统中有上百个小策略类。后来通过将相关策略组合成更大的策略,并使用工厂方法按需创建,显著改善了代码结构。
8. C++20中的新特性应用
C++20引入了一些新特性,可以让我们更优雅地实现策略模式:
8.1 使用概念约束策略类型
cpp复制template<typename T>
concept SortingStrategy = requires(T t, std::vector<int>& v) {
{ t.sort(v) } -> std::same_as<void>;
};
template<SortingStrategy Strategy>
class GenericSorter {
Strategy strategy;
public:
void execute(std::vector<int>& data) {
strategy.sort(data);
}
};
8.2 使用span替代vector&
cpp复制class ModernSortingStrategy {
public:
virtual void sort(std::span<int> data) = 0;
virtual ~ModernSortingStrategy() = default;
};
8.3 协程与策略模式
对于需要暂停和恢复的长时间运行算法,可以将策略模式与协程结合:
cpp复制class AsyncStrategy {
public:
virtual std::future<void> executeAsync() = 0;
virtual ~AsyncStrategy() = default;
};
class DataProcessingStrategy : public AsyncStrategy {
public:
std::future<void> executeAsync() override {
co_await std::suspend_always{};
// 异步处理逻辑
co_return;
}
};
9. 从设计模式到设计原则
策略模式体现了几个重要的面向对象设计原则:
- 开闭原则(OCP):对扩展开放,对修改关闭
- 单一职责原则(SRP):每个策略类只负责一个算法
- 依赖倒置原则(DIP):高层模块不依赖低层模块,二者都依赖抽象
- 组合优于继承:通过对象组合而非类继承来复用行为
理解这些原则比记住模式本身更重要。在实际编码时,我通常会先思考这些原则,然后自然推导出合适的设计模式应用。
10. 现代C++工程实践建议
10.1 代码组织
对于大型项目中的策略模式实现,建议:
- 将策略接口和主要上下文放在核心模块
- 具体策略实现按功能分组到不同模块
- 使用命名空间组织相关策略
code复制project/
├── core/
│ ├── strategies/
│ │ ├── sorting.h
│ │ └── shipping.h
│ └── context.h
├── implementations/
│ ├── sorting/
│ │ ├── quicksort.h
│ │ └── mergesort.h
│ └── shipping/
│ ├── standard.h
│ └── vip.h
└── main.cpp
10.2 编译时策略选择
对于性能关键代码,可以考虑编译时策略选择:
cpp复制template<typename Strategy>
class CompileTimeSorter {
Strategy strategy;
public:
void execute(std::vector<int>& data) {
strategy.sort(data);
}
};
// 使用示例
CompileTimeSorter<QuickSort> sorter;
sorter.execute(data);
10.3 跨平台策略实现
在跨平台开发中,策略模式可以很好地封装平台相关代码:
cpp复制class PlatformStrategy {
public:
virtual void* allocate(size_t size) = 0;
virtual void deallocate(void* ptr) = 0;
virtual ~PlatformStrategy() = default;
};
class WindowsMemoryStrategy : public PlatformStrategy {
void* allocate(size_t size) override {
return VirtualAlloc(nullptr, size, MEM_COMMIT, PAGE_READWRITE);
}
void deallocate(void* ptr) override {
VirtualFree(ptr, 0, MEM_RELEASE);
}
};
class PosixMemoryStrategy : public PlatformStrategy {
void* allocate(size_t size) override {
return mmap(nullptr, size, PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
}
void deallocate(void* ptr) override {
munmap(ptr, /* size */);
}
};
11. 性能对比:虚函数 vs 函数指针 vs std::function
在选择策略模式实现方式时,了解不同方法的性能特征很重要:
- 虚函数:传统的OOP方式,有间接调用开销
- 函数指针:C风格,轻量但缺乏灵活性
- std::function:灵活但可能有分配开销
- 模板:零开销,但编译时代码膨胀
以下是一个简单的性能测试框架:
cpp复制void benchmark() {
constexpr size_t iterations = 1'000'000;
std::vector<int> data(1000);
// 虚函数测试
{
VirtualStrategy* strategy = new ConcreteStrategy();
auto start = std::chrono::high_resolution_clock::now();
for (size_t i = 0; i < iterations; ++i) {
strategy->execute(data);
}
auto end = std::chrono::high_resolution_clock::now();
std::cout << "Virtual: " << (end - start).count() << " ns\n";
delete strategy;
}
// std::function测试
{
std::function<void(std::vector<int>&)> func = [](auto& data) {
std::sort(data.begin(), data.end());
};
auto start = std::chrono::high_resolution_clock::now();
for (size_t i = 0; i < iterations; ++i) {
func(data);
}
auto end = std::chrono::high_resolution_clock::now();
std::cout << "std::function: " << (end - start).count() << " ns\n";
}
// 模板测试
{
TemplateSorter<QuickSort> sorter;
auto start = std::chrono::high_resolution_clock::now();
for (size_t i = 0; i < iterations; ++i) {
sorter.execute(data);
}
auto end = std::chrono::high_resolution_clock::now();
std::cout << "Template: " << (end - start).count() << " ns\n";
}
}
在实际项目中,我通常会根据具体情况选择:
- 需要运行时多态:虚函数或std::function
- 性能关键路径:模板或函数指针
- 需要捕获状态的策略:std::function
12. 策略模式在游戏开发中的应用
游戏开发是策略模式的典型应用场景。以下是一个游戏AI行为控制的例子:
cpp复制// AI行为策略
class AIBehavior {
public:
virtual void update(float deltaTime, GameObject& actor) = 0;
virtual ~AIBehavior() = default;
};
// 具体行为策略
class PatrolBehavior : public AIBehavior {
void update(float deltaTime, GameObject& actor) override {
// 巡逻逻辑
}
};
class ChaseBehavior : public AIBehavior {
void update(float deltaTime, GameObject& actor) override {
// 追逐玩家逻辑
}
};
class FleeBehavior : public AIBehavior {
void update(float deltaTime, GameObject& actor) override {
// 逃跑逻辑
}
};
// AI控制器
class AIController {
std::unique_ptr<AIBehavior> currentBehavior;
public:
void setBehavior(std::unique_ptr<AIBehavior> newBehavior) {
currentBehavior = std::move(newBehavior);
}
void update(float deltaTime, GameObject& actor) {
if (currentBehavior) {
currentBehavior->update(deltaTime, actor);
}
}
};
这种设计允许:
- 动态改变NPC行为(如从巡逻切换到追逐)
- 容易添加新行为类型
- 复用相同行为于不同NPC
13. 策略模式在GUI框架中的应用
GUI框架中经常使用策略模式来处理不同平台的具体实现:
cpp复制// 绘图策略接口
class DrawingStrategy {
public:
virtual void drawLine(Point from, Point to) = 0;
virtual void drawCircle(Point center, float radius) = 0;
virtual ~DrawingStrategy() = default;
};
// 具体平台实现
class WindowsDrawing : public DrawingStrategy {
void drawLine(Point from, Point to) override {
// 调用Windows API
}
void drawCircle(Point center, float radius) override {
// 调用Windows API
}
};
class MacDrawing : public DrawingStrategy {
void drawLine(Point from, Point to) override {
// 调用macOS API
}
void drawCircle(Point center, float radius) override {
// 调用macOS API
}
};
// 图形上下文
class GraphicsContext {
std::unique_ptr<DrawingStrategy> drawer;
public:
explicit GraphicsContext(std::unique_ptr<DrawingStrategy> strategy)
: drawer(std::move(strategy)) {}
void line(Point from, Point to) {
drawer->drawLine(from, to);
}
void circle(Point center, float radius) {
drawer->drawCircle(center, radius);
}
};
14. 策略模式与依赖注入
策略模式常与依赖注入(DI)结合使用,特别是在大型应用程序中:
cpp复制// 使用依赖注入框架注册策略
class AppModule : public di::Module {
public:
void configure() override {
bind<ShippingStrategy>().to<StandardShipping>();
bind<LogStrategy>().to<FileLogger>();
// ...
}
};
// 客户端代码通过DI容器获取策略
class OrderService {
std::shared_ptr<ShippingStrategy> shippingStrategy;
public:
// 通过构造函数注入
explicit OrderService(std::shared_ptr<ShippingStrategy> strategy)
: shippingStrategy(std::move(strategy)) {}
double calculateShipping(const Order& order) {
return shippingStrategy->calculate(order);
}
};
这种方式的优点:
- 解耦策略创建和使用
- 便于单元测试(可以注入模拟策略)
- 集中管理策略生命周期
15. 策略模式在算法竞赛中的应用
即使是算法竞赛中的简单问题,策略模式也能提高代码的清晰度和可维护性。考虑热词中提到的洛谷P5708问题:
cpp复制// 面积计算策略
class AreaCalculator {
public:
virtual double calculate(double a, double b, double c) = 0;
virtual ~AreaCalculator() = default;
};
// 海伦公式实现
class HeronFormula : public AreaCalculator {
public:
double calculate(double a, double b, double c) override {
double p = (a + b + c) / 2;
return sqrt(p * (p - a) * (p - b) * (p - c));
}
};
// 上下文
class Triangle {
AreaCalculator* calculator;
double a, b, c;
public:
Triangle(double a, double b, double c, AreaCalculator* calc)
: a(a), b(b), c(c), calculator(calc) {}
double area() {
return calculator->calculate(a, b, c);
}
};
// 使用示例
int main() {
double a, b, c;
std::cin >> a >> b >> c;
HeronFormula formula;
Triangle triangle(a, b, c, &formula);
std::cout << std::fixed << std::setprecision(1)
<< triangle.area() << std::endl;
return 0;
}
虽然对于简单问题这可能显得过度设计,但对于复杂算法组合或需要频繁切换算法的情况,这种结构会很有优势。
16. 策略模式与SOLID原则
策略模式很好地体现了SOLID原则:
- 单一职责原则:每个策略类只负责一个算法或行为
- 开闭原则:可以添加新策略而不修改现有代码
- 里氏替换原则:所有具体策略都可以替换基类策略
- 接口隔离原则:策略接口应该保持小而专注
- 依赖倒置原则:高层模块依赖抽象策略接口
在实际项目中,我经常用这些原则来验证设计决策。如果一个设计违反了这些原则,策略模式往往能提供解决方案。
17. 策略模式的反模式与误用
虽然策略模式很强大,但也可能被误用:
-
策略爆炸:创建过多小策略类,导致系统复杂
- 解决方案:合并相关策略,或使用其他模式
-
上下文过载:上下文类承担太多与策略交互的责任
- 解决方案:将部分职责移回策略类
-
过度抽象:为永远不会变化的简单行为使用策略模式
- 解决方案:YAGNI原则,需要时才引入
-
性能陷阱:在性能关键路径上使用虚函数策略
- 解决方案:考虑编译时策略或CRTP
我曾经在一个项目中看到过200多个小策略类,维护起来非常困难。后来通过引入策略组合和层次结构,将数量减少到30个左右,同时保持了灵活性。
18. C++策略模式的未来演进
随着C++标准的发展,策略模式可能有新的实现方式:
- 模式匹配(C++23+):可能提供新的策略选择语法
- 契约(Contracts):可以更好地验证策略前提条件
- 反射:可能实现更动态的策略注册和发现
例如,未来的模式匹配可能简化策略选择:
cpp复制// 可能的未来C++代码
ShippingStrategy selectStrategy(const Order& order) {
return inspect(order) {
[.isVIP = true] => VIPShipping{};
[.isExpress = true] => ExpressShipping{};
_ => StandardShipping{};
};
}
19. 从策略模式到策略引擎
在复杂系统中,策略模式可以进化为策略引擎:
- 规则引擎:基于条件的策略自动选择
- 策略链:多个策略按顺序应用
- 策略组合:将简单策略组合成复杂策略
cpp复制// 策略链示例
class CompositeShippingStrategy : public ShippingStrategy {
std::vector<std::unique_ptr<ShippingStrategy>> strategies;
public:
void addStrategy(std::unique_ptr<ShippingStrategy> strategy) {
strategies.push_back(std::move(strategy));
}
double calculate(const Order& order) override {
double result = 0;
for (auto& strategy : strategies) {
result += strategy->calculate(order);
}
return result;
}
};
// 使用示例
auto composite = std::make_unique<CompositeShippingStrategy>();
composite->addStrategy(std::make_unique<BaseShipping>());
composite->addStrategy(std::make_unique<InsuranceStrategy>());
ShippingCalculator calculator(std::move(composite));
20. 总结与个人实践心得
策略模式是我在C++项目中最常用的设计模式之一。经过多年实践,我总结出以下几点经验:
-
识别变化点:策略模式应该封装系统中真正可能变化的部分,而不是为所有行为都创建策略。
-
平衡灵活性与复杂性:不是所有地方都需要策略模式,在简单场景中直接实现可能更合适。
-
生命周期管理:在C++中要特别注意策略对象的所有权和生命周期,优先使用智能指针。
-
性能考量:在性能敏感路径上,考虑使用编译时策略或静态分发。
-
测试友好:策略模式天然支持单元测试,可以为每个策略编写独立测试。
-
文档重要:特别是当策略之间有微妙差异时,清晰的文档可以避免误用。
最后分享一个实用技巧:在大型项目中,我通常会为策略接口编写详细的注释,说明每个策略应该实现什么、不应该实现什么,以及典型的使用场景。这大大提高了代码的可维护性。
