策略模式详解:把算法"装"进盒子里随意切换,C++完整实现
引言
你每天上班会选择什么出行方式?
- 时间充裕且天气好:骑自行车,环保又健身
- 赶时间但预算有限:坐地铁,快速又便宜
- 下雨或者带了很多东西:打车,舒适又方便
- 公司有急事:开车,最灵活
如果用代码来实现这个逻辑,你可能会写出这样的代码:
cpp
void goToWork(const std::string& way) {
if (way == "bike") {
std::cout << "骑自行车去上班" << std::endl;
// 骑自行车的具体逻辑...
} else if (way == "subway") {
std::cout << "坐地铁去上班" << std::endl;
// 坐地铁的具体逻辑...
} else if (way == "taxi") {
std::cout << "打车去上班" << std::endl;
// 打车的具体逻辑...
} else if (way == "car") {
std::cout << "开车去上班" << std::endl;
// 开车的具体逻辑...
}
}
这段代码看起来很简单,但存在严重的问题:
- 违反开闭原则 :新增一种出行方式(比如共享单车),必须修改
goToWork函数 - 代码臃肿:所有出行方式的逻辑都挤在一个函数里,难以维护
- 无法动态切换:一旦选择了一种方式,中途不能改变
- 复用性差:如果其他地方也需要用到打车逻辑,只能复制粘贴
策略模式(Strategy Pattern) 正是为了解决这个问题而生的。它是一种行为型设计模式,定义了一系列算法,将每个算法封装起来,并使它们可以互相替换。策略模式让算法的变化独立于使用算法的客户端。
今天我们就用C++语言,从基础概念到完整实现,彻底搞懂策略模式。
一、策略模式的核心概念
1.1 解决的痛点
策略模式主要解决以下问题:
- 多重条件判断的噩梦 :大量的
if-else或switch-case语句,代码难以阅读和维护 - 算法与使用代码耦合:算法的实现和使用代码混在一起,修改算法可能会影响使用代码
- 无法动态切换算法:算法在编译时就确定了,运行时不能改变
- 算法复用性差:相同的算法在多个地方使用时,只能复制粘贴
1.2 核心思想
策略模式的核心思想是:"封装变化"。把会变化的部分(算法/行为)从不变的部分(使用算法的代码)中分离出来,封装成独立的类。这些类实现同一个接口,因此可以互相替换。
这样,当需要新增或修改算法时,只需要新增或修改对应的策略类,不需要修改使用算法的代码。客户端可以在运行时动态选择使用哪个策略。
1.3 三个核心角色
策略模式包含三个关键角色:
- 抽象策略(Strategy):定义了所有具体策略必须实现的接口,声明了算法的方法
- 具体策略(Concrete Strategy):实现了抽象策略接口,包含具体的算法逻辑
- 上下文(Context):持有一个策略对象的引用,负责将客户端的请求委托给具体的策略对象执行。它还可以提供一些通用的逻辑,或者在调用策略前后做一些预处理和后处理
二、标准策略模式实现
2.1 UML类图


2.2 C++实现(出行方式例子)
我们就用开头提到的出行方式例子来实现策略模式。
cpp
#include <iostream>
#include <string>
#include <memory>
// 抽象策略:出行策略
class TravelStrategy {
public:
virtual ~TravelStrategy() = default;
virtual void travel() const = 0; // 出行算法
};
// 具体策略1:骑自行车
class BikeStrategy : public TravelStrategy {
public:
void travel() const override {
std::cout << "🚲 骑自行车去上班" << std::endl;
std::cout << " 优点:环保、健身、不堵车" << std::endl;
std::cout << " 耗时:约30分钟" << std::endl;
}
};
// 具体策略2:坐地铁
class SubwayStrategy : public TravelStrategy {
public:
void travel() const override {
std::cout << "🚇 坐地铁去上班" << std::endl;
std::cout << " 优点:快速、便宜、准时" << std::endl;
std::cout << " 耗时:约20分钟" << std::endl;
}
};
// 具体策略3:打车
class TaxiStrategy : public TravelStrategy {
public:
void travel() const override {
std::cout << "🚕 打车去上班" << std::endl;
std::cout << " 优点:舒适、门到门" << std::endl;
std::cout << " 耗时:约15分钟(不堵车)" << std::endl;
}
};
// 具体策略4:开车
class CarStrategy : public TravelStrategy {
public:
void travel() const override {
std::cout << "🚗 开车去上班" << std::endl;
std::cout << " 优点:灵活、可以带东西" << std::endl;
std::cout << " 耗时:约25分钟(视路况而定)" << std::endl;
}
};
// 上下文:出行上下文
class TravelContext {
private:
std::unique_ptr<TravelStrategy> strategy_; // 持有策略对象
public:
// 构造函数可以接收一个默认策略
explicit TravelContext(std::unique_ptr<TravelStrategy> strategy = nullptr)
: strategy_(std::move(strategy)) {}
// 动态设置策略
void setStrategy(std::unique_ptr<TravelStrategy> strategy) {
strategy_ = std::move(strategy);
}
// 执行出行
void goToWork() const {
if (!strategy_) {
std::cout << "❌ 没有设置出行策略!" << std::endl;
return;
}
std::cout << "准备去上班..." << std::endl;
strategy_->travel(); // 委托给具体策略执行
std::cout << "到达公司!" << std::endl;
}
};
// 客户端代码
int main() {
TravelContext context;
std::cout << "=== 周一:天气好,骑自行车 ===" << std::endl;
context.setStrategy(std::make_unique<BikeStrategy>());
context.goToWork();
std::cout << "\n=== 周二:起晚了,打车 ===" << std::endl;
context.setStrategy(std::make_unique<TaxiStrategy>());
context.goToWork();
std::cout << "\n=== 周三:正常上班,坐地铁 ===" << std::endl;
context.setStrategy(std::make_unique<SubwayStrategy>());
context.goToWork();
std::cout << "\n=== 周四:带了很多东西,开车 ===" << std::endl;
context.setStrategy(std::make_unique<CarStrategy>());
context.goToWork();
return 0;
}
2.3 运行结果
=== 周一:天气好,骑自行车 ===
准备去上班...
🚲 骑自行车去上班
优点:环保、健身、不堵车
耗时:约30分钟
到达公司!
=== 周二:起晚了,打车 ===
准备去上班...
🚕 打车去上班
优点:舒适、门到门
耗时:约15分钟(不堵车)
到达公司!
=== 周三:正常上班,坐地铁 ===
准备去上班...
🚇 坐地铁去上班
优点:快速、便宜、准时
耗时:约20分钟
到达公司!
=== 周四:带了很多东西,开车 ===
准备去上班...
🚗 开车去上班
优点:灵活、可以带东西
耗时:约25分钟(视路况而定)
到达公司!
2.4 代码解析
- 抽象策略
TravelStrategy:定义了所有出行策略必须实现的travel()方法 - 具体策略 :
BikeStrategy、SubwayStrategy、TaxiStrategy、CarStrategy,每个都实现了自己的出行逻辑 - 上下文
TravelContext:持有一个策略对象的引用,提供了setStrategy()方法可以动态更换策略,goToWork()方法将实际的出行逻辑委托给策略对象执行
关键优势:
- 新增一种出行方式,只需要新增一个具体策略类,不需要修改任何现有代码
- 可以在运行时动态切换策略
- 每个策略类职责单一,易于维护和测试
- 策略可以在多个地方复用
三、策略模式的进阶用法
3.1 带参数的策略
有时候策略需要接收一些参数来执行算法。我们可以在策略方法中添加参数:
cpp
// 抽象策略:折扣策略
class DiscountStrategy {
public:
virtual ~DiscountStrategy() = default;
// 计算折扣后的价格,接收原价作为参数
virtual double calculate(double originalPrice) const = 0;
};
// 具体策略:无折扣
class NoDiscount : public DiscountStrategy {
public:
double calculate(double originalPrice) const override {
return originalPrice;
}
};
// 具体策略:百分比折扣
class PercentageDiscount : public DiscountStrategy {
private:
double percentage_; // 折扣百分比,比如0.8表示8折
public:
explicit PercentageDiscount(double percentage)
: percentage_(percentage) {}
double calculate(double originalPrice) const override {
return originalPrice * percentage_;
}
};
// 具体策略:满减折扣
class FullReductionDiscount : public DiscountStrategy {
private:
double full_; // 满多少
double reduction_; // 减多少
public:
FullReductionDiscount(double full, double reduction)
: full_(full), reduction_(reduction) {}
double calculate(double originalPrice) const override {
if (originalPrice >= full_) {
return originalPrice - reduction_;
}
return originalPrice;
}
};
// 上下文:购物车
class ShoppingCart {
private:
std::unique_ptr<DiscountStrategy> discount_strategy_;
double total_ = 0.0;
public:
void setDiscountStrategy(std::unique_ptr<DiscountStrategy> strategy) {
discount_strategy_ = std::move(strategy);
}
void addItem(double price) {
total_ += price;
}
double checkout() const {
if (!discount_strategy_) {
return total_;
}
return discount_strategy_->calculate(total_);
}
};
// 客户端代码
int main() {
ShoppingCart cart;
cart.addItem(100.0);
cart.addItem(200.0);
cart.addItem(300.0);
std::cout << "原价: " << cart.checkout() << "元" << std::endl;
cart.setDiscountStrategy(std::make_unique<PercentageDiscount>(0.8));
std::cout << "8折后: " << cart.checkout() << "元" << std::endl;
cart.setDiscountStrategy(std::make_unique<FullReductionDiscount>(500.0, 100.0));
std::cout << "满500减100后: " << cart.checkout() << "元" << std::endl;
return 0;
}
3.2 上下文添加通用逻辑
上下文不仅可以委托策略执行算法,还可以在调用策略前后添加一些通用的逻辑,比如日志记录、性能统计、参数校验等:
cpp
class TravelContext {
private:
std::unique_ptr<TravelStrategy> strategy_;
public:
// ... 其他方法不变
void goToWork() const {
if (!strategy_) {
std::cout << "❌ 没有设置出行策略!" << std::endl;
return;
}
// 预处理:记录开始时间
auto start = std::chrono::high_resolution_clock::now();
std::cout << "准备去上班..." << std::endl;
strategy_->travel(); // 委托给具体策略执行
std::cout << "到达公司!" << std::endl;
// 后处理:记录耗时
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
std::cout << "本次出行耗时: " << duration.count() << "ms" << std::endl;
}
};
这种方式非常优雅,通用逻辑只需要在上下文中写一次,所有策略都能受益。
四、策略模式的优缺点
4.1 优点
- 算法可以自由切换:可以在运行时动态更换策略
- 避免使用多重条件判断 :消除了大量的
if-else或switch-case语句 - 扩展性好:新增策略只需要新增一个类,符合开闭原则
- 符合单一职责原则:每个策略类只负责一个算法,易于维护和测试
- 算法复用性高:策略可以在多个不同的上下文和客户端中复用
- 隔离算法细节:客户端不需要知道算法的具体实现细节,只需要知道策略的接口
4.2 缺点
- 策略类数量增多:每个算法都需要一个单独的类,会导致系统中类的数量增加
- 客户端需要了解所有策略:客户端必须知道所有的具体策略类,才能选择合适的策略
- 所有策略都需要暴露:抽象策略接口必须声明所有策略需要的方法,即使有些策略不需要某些方法
- 不适合算法差异过大的情况:如果不同策略的算法差异非常大,抽象策略接口可能会变得臃肿
五、适用场景
策略模式特别适合以下场景:
- 多个类只有算法不同的情况:比如不同的排序算法、不同的加密算法、不同的压缩算法
- 需要动态切换算法的情况:比如根据不同的用户等级使用不同的折扣策略,根据不同的网络环境使用不同的传输策略
- 需要屏蔽算法具体实现细节的情况:让客户端只关心算法的接口,不关心实现
- 多重条件判断的情况 :当代码中出现大量的
if-else或switch-case来选择不同的算法时 - 算法需要独立于客户端变化的情况:算法的修改不应该影响到使用算法的客户端
经典应用案例:
- 支付系统:支持微信支付、支付宝、银行卡等多种支付方式
- 电商系统:多种折扣策略(满减、折扣、立减、赠品等)
- 游戏开发:不同的AI行为策略(进攻、防守、逃跑等)
- 日志系统:不同的日志输出方式(控制台、文件、数据库、网络等)
- 排序算法:根据数据量大小选择不同的排序算法
六、与其他模式的对比
很多人容易把策略模式和其他一些设计模式混淆,这里做一个清晰的对比:
| 模式 | 核心目的 | 与策略模式的区别 |
|---|---|---|
| 策略模式 | 封装算法,使算法可以互相替换 | 关注行为的替换,算法之间是平等的,可以任意切换 |
| 简单工厂模式 | 创建对象 | 关注对象的创建,创建后对象的行为是固定的 |
| 状态模式 | 封装状态,根据状态改变行为 | 状态之间有转换关系,行为的改变是由状态驱动的 |
| 装饰器模式 | 动态给对象添加功能 | 不改变接口,增强功能,支持多层嵌套 |
| 模板方法模式 | 定义算法的骨架,子类实现具体步骤 | 算法的骨架是固定的,只有部分步骤可以变化 |
最容易混淆的是策略模式和状态模式,它们的核心区别在于:
- 策略模式:客户端主动选择使用哪个策略,策略之间没有依赖关系
- 状态模式:状态的改变是自动的,由上下文内部的条件触发,状态之间有转换关系
七、现代C++改进建议
在现代C++(C++11及以后)中,我们可以对策略模式进行一些改进,让代码更加简洁和灵活。
7.1 使用std::function和Lambda简化策略
对于只有一个方法的简单策略,我们可以使用std::function和Lambda表达式来代替抽象策略类和具体策略类,代码会非常简洁:
cpp
#include <functional>
// 上下文:出行上下文
class TravelContext {
private:
// 使用std::function作为策略类型
using TravelStrategy = std::function<void()>;
TravelStrategy strategy_;
public:
void setStrategy(TravelStrategy strategy) {
strategy_ = std::move(strategy);
}
void goToWork() const {
if (!strategy_) {
std::cout << "❌ 没有设置出行策略!" << std::endl;
return;
}
std::cout << "准备去上班..." << std::endl;
strategy_(); // 调用函数对象
std::cout << "到达公司!" << std::endl;
}
};
// 客户端代码
int main() {
TravelContext context;
// 直接使用Lambda表达式作为策略
context.setStrategy([]() {
std::cout << "🚲 骑自行车去上班" << std::endl;
std::cout << " 优点:环保、健身、不堵车" << std::endl;
});
context.goToWork();
context.setStrategy([]() {
std::cout << "🚇 坐地铁去上班" << std::endl;
std::cout << " 优点:快速、便宜、准时" << std::endl;
});
context.goToWork();
return 0;
}
这种方式非常适合简单的策略场景,不需要定义任何类,直接使用Lambda表达式即可。
7.2 使用模板实现通用上下文
我们可以使用模板来实现一个通用的上下文类,适用于任何策略接口:
cpp
template <typename Strategy>
class Context {
private:
std::unique_ptr<Strategy> strategy_;
public:
explicit Context(std::unique_ptr<Strategy> strategy = nullptr)
: strategy_(std::move(strategy)) {}
void setStrategy(std::unique_ptr<Strategy> strategy) {
strategy_ = std::move(strategy);
}
template <typename... Args>
auto execute(Args&&... args) const {
if (!strategy_) {
throw std::runtime_error("No strategy set");
}
return strategy_->execute(std::forward<Args>(args)...);
}
};
// 使用
class MyStrategy {
public:
virtual int execute(int a, int b) const = 0;
};
class AddStrategy : public MyStrategy {
public:
int execute(int a, int b) const override {
return a + b;
}
};
int main() {
Context<MyStrategy> context;
context.setStrategy(std::make_unique<AddStrategy>());
std::cout << context.execute(1, 2) << std::endl; // 输出3
return 0;
}
7.3 结合工厂模式创建策略
为了解决客户端需要知道所有具体策略类的问题,我们可以结合工厂模式来创建策略对象。客户端只需要传入一个策略标识,工厂就会创建对应的策略对象:
cpp
// 策略工厂
class StrategyFactory {
public:
static std::unique_ptr<TravelStrategy> createStrategy(const std::string& type) {
if (type == "bike") {
return std::make_unique<BikeStrategy>();
} else if (type == "subway") {
return std::make_unique<SubwayStrategy>();
} else if (type == "taxi") {
return std::make_unique<TaxiStrategy>();
} else if (type == "car") {
return std::make_unique<CarStrategy>();
} else {
return nullptr;
}
}
};
// 客户端代码
int main() {
TravelContext context;
context.setStrategy(StrategyFactory::createStrategy("bike"));
context.goToWork();
context.setStrategy(StrategyFactory::createStrategy("taxi"));
context.goToWork();
return 0;
}
这样客户端就不需要知道任何具体的策略类,只需要知道策略的标识即可。
八、实际应用中的注意事项
- 策略数量不宜过多:如果策略数量超过5-6个,可能会导致系统复杂度增加。这时候可以考虑使用享元模式来共享策略对象,或者使用其他模式
- 策略应该是无状态的:策略对象不应该持有自己的状态,这样可以在多个上下文之间共享同一个策略对象,提高性能
- 正确处理空策略:上下文应该处理策略为空的情况,避免空指针异常
- 考虑策略的优先级:如果多个策略可以同时适用,需要考虑策略的优先级问题
- 避免过度使用:如果算法非常简单且不会变化,就没有必要使用策略模式,直接写在代码里即可
九、总结
策略模式是一种非常实用的设计模式,它的核心思想是**"封装变化,委托执行"**。通过将算法封装成独立的策略类,我们可以实现算法的自由切换,消除多重条件判断,提高代码的扩展性和可维护性。
在实际开发中,当你发现代码中有大量的if-else或switch-case语句来选择不同的算法时,就应该考虑使用策略模式来重构。结合工厂模式和现代C++的std::function、Lambda表达式,可以让策略模式的实现更加简洁和优雅。
记住,设计模式不是银弹。只有当算法确实会变化,并且需要动态切换时,才应该使用策略模式。如果算法是固定的,不会变化,那么直接写在代码里会更简单。
希望这篇文章能帮助你彻底理解策略模式,并在实际项目中正确地使用它。