1. 策略模式的核心价值与应用场景
在C++开发中,策略模式(Strategy Pattern)是我最常使用的设计模式之一。记得刚入行时,我接手过一个电商促销系统,里面充斥着各种if-else判断用户类型来计算折扣的代码。每次新增促销活动都要修改核心逻辑,测试覆盖率直线下降,那段经历让我深刻认识到策略模式的价值。
策略模式的本质是将算法家族封装起来,让它们可以互相替换。这种模式让算法的变化独立于使用算法的客户。在以下场景中特别适用:
- 当存在多种算法变体,且需要在运行时切换时
- 当需要消除复杂的条件分支语句时
- 当算法实现需要被多个不同对象使用时
提示:策略模式与简单工厂模式的区别在于,工厂关注对象创建,而策略关注行为封装和运行时切换。
2. 策略模式的三大核心组件
2.1 抽象策略接口设计
抽象策略类是整个模式的基础,它定义了所有具体策略必须实现的接口。在C++中,我们通常使用纯虚函数来定义这个接口:
cpp复制class DiscountStrategy {
public:
virtual double calculate(double originalPrice) = 0;
virtual ~DiscountStrategy() = default; // 虚析构确保正确释放资源
};
这里有几个关键设计点:
- 接口函数应该足够通用,能覆盖所有策略变体
- 必须声明虚析构函数,避免通过基类指针删除子类对象时出现资源泄漏
- 接口命名应该明确表达其功能,calculate比compute更贴近业务语义
2.2 具体策略实现要点
具体策略类实现抽象接口,每个类封装一种算法变体。以电商折扣为例:
cpp复制class SuperVipUserDiscount : public DiscountStrategy {
public:
double calculate(double originalPrice) override {
double price = originalPrice * 0.8;
if (price >= 100) price -= 10; // 满减条件判断
return price;
}
};
实现时需要注意:
- 每个策略类应该只关注自己的算法逻辑,不依赖外部状态
- 策略类通常是无状态的,可以被多个上下文共享
- 复杂策略可以考虑使用组合模式,将多个简单策略组合使用
2.3 上下文类的关键实现
上下文类持有策略对象的引用,是连接客户端和策略的桥梁:
cpp复制class Order {
private:
std::unique_ptr<DiscountStrategy> strategy_;
double price_;
public:
Order(double price, std::unique_ptr<DiscountStrategy> strategy)
: price_(price), strategy_(std::move(strategy)) {}
void setStrategy(std::unique_ptr<DiscountStrategy> strategy) {
strategy_ = std::move(strategy);
}
double getFinalPrice() {
if (!strategy_) throw std::runtime_error("Strategy not set");
return strategy_->calculate(price_);
}
};
上下文类的设计要点:
- 使用智能指针管理策略对象生命周期
- 提供清晰的策略设置接口
- 对策略调用进行必要的参数校验
- 隐藏策略实现细节,对外提供统一接口
3. 策略模式的高级应用技巧
3.1 策略工厂的优化实现
当策略类型较多时,可以使用工厂模式来简化策略对象的创建:
cpp复制class DiscountStrategyFactory {
public:
static std::unique_ptr<DiscountStrategy> create(UserType type) {
switch(type) {
case UserType::NORMAL:
return std::make_unique<NormalUserDiscount>();
case UserType::VIP:
return std::make_unique<VipUserDiscount>();
case UserType::SUPER_VIP:
return std::make_unique<SuperVipUserDiscount>();
default:
throw std::invalid_argument("Unknown user type");
}
}
};
工厂模式的优点:
- 集中管理策略创建逻辑
- 客户端代码与具体策略类解耦
- 便于统一修改策略创建方式
3.2 基于Lambda的轻量级策略
对于简单策略,可以使用C++11的lambda表达式替代策略类:
cpp复制auto weekendDiscount = [](double price) {
return price * 0.85; // 周末85折
};
Order order(100.0, std::make_unique<LambdaStrategy>(weekendDiscount));
需要定义一个适配器类来包装lambda:
cpp复制class LambdaStrategy : public DiscountStrategy {
std::function<double(double)> func_;
public:
LambdaStrategy(std::function<double(double)> f) : func_(f) {}
double calculate(double price) override { return func_(price); }
};
3.3 策略模式与模板的结合
对于性能敏感的场景,可以使用模板策略模式:
cpp复制template<typename Strategy>
class OrderTemplate {
Strategy strategy_;
double price_;
public:
OrderTemplate(double price) : price_(price) {}
double getFinalPrice() { return strategy_.calculate(price_); }
};
这种方式的优点:
- 避免了虚函数调用的开销
- 编译时多态,性能更高
- 策略类型在编译期确定
缺点:
- 无法运行时动态切换策略
- 代码膨胀风险
4. 实战中的常见问题与解决方案
4.1 策略选择与性能权衡
在实际项目中,我们需要根据场景选择合适的策略实现方式:
| 实现方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 经典虚函数 | 需要运行时动态切换 | 灵活,符合开闭原则 | 虚函数调用开销 |
| 模板策略 | 性能敏感,策略固定 | 无运行时开销 | 无法动态切换 |
| Lambda策略 | 简单临时策略 | 代码简洁 | 复杂策略可读性差 |
4.2 策略对象的生命周期管理
C++中策略对象的生命周期管理至关重要,常见问题包括:
-
内存泄漏:忘记释放策略对象
- 解决方案:使用智能指针(unique_ptr/shared_ptr)
-
悬空指针:策略对象被提前释放
- 解决方案:明确所有权关系,避免跨作用域传递裸指针
-
线程安全问题:多线程共享策略对象
- 解决方案:确保策略类是无状态的,或添加适当的同步机制
4.3 策略模式的测试策略
为策略模式设计单元测试时,应该:
- 单独测试每个具体策略类
- 测试上下文类与不同策略的交互
- 验证策略切换的正确性
- 检查边界条件和异常情况
示例测试用例:
cpp复制TEST(OrderTest, StrategySwitch) {
Order order(100.0, std::make_unique<NormalUserDiscount>());
ASSERT_EQ(100.0, order.getFinalPrice());
order.setStrategy(std::make_unique<VipUserDiscount>());
ASSERT_EQ(90.0, order.getFinalPrice());
}
5. 策略模式在复杂系统中的应用
5.1 组合策略模式
当单个策略无法满足复杂业务需求时,可以使用组合策略:
cpp复制class CompositeDiscount : public DiscountStrategy {
std::vector<std::unique_ptr<DiscountStrategy>> strategies_;
public:
void addStrategy(std::unique_ptr<DiscountStrategy> strategy) {
strategies_.push_back(std::move(strategy));
}
double calculate(double price) override {
for (auto& strategy : strategies_) {
price = strategy->calculate(price);
}
return price;
}
};
这种模式常见于:
- 多级会员折扣系统
- 叠加促销活动
- 分阶段计价策略
5.2 策略模式与其它模式的结合
-
策略+装饰器模式:
- 装饰器动态添加职责
- 策略动态改变算法
-
策略+状态模式:
- 状态模式处理状态转移
- 策略模式处理状态对应的行为
-
策略+命令模式:
- 命令模式封装请求
- 策略模式封装算法
5.3 策略模式的性能优化
对于高频调用的策略场景,可以考虑:
- 策略对象池:复用策略对象,减少内存分配
- 内联策略:简单策略直接内联实现
- 基于策略的编译时选择:使用模板元编程
cpp复制template<DiscountType TYPE>
class DiscountTraits;
template<>
class DiscountTraits<DiscountType::VIP> {
public:
static double apply(double price) { return price * 0.9; }
};
template<DiscountType TYPE>
double applyDiscount(double price) {
return DiscountTraits<TYPE>::apply(price);
}
6. 策略模式的替代方案与比较
虽然策略模式很强大,但并不是所有情况都适用。以下是几种常见替代方案:
6.1 简单工厂+条件判断
cpp复制double calculateDiscount(UserType type, double price) {
switch(type) {
case UserType::VIP: return price * 0.9;
case UserType::SUPER_VIP: return price * 0.8;
default: return price;
}
}
适用场景:
- 策略数量固定且很少变化
- 算法简单,不需要单独封装
6.2 模板特化
cpp复制template<UserType TYPE>
class DiscountCalculator;
template<>
class DiscountCalculator<UserType::VIP> {
public:
static double calculate(double price) { return price * 0.9; }
};
适用场景:
- 策略在编译期确定
- 需要极致性能
6.3 函数指针表
cpp复制using DiscountFunc = double(*)(double);
const DiscountFunc discountTable[] = {
[](double p){return p;}, // Normal
[](double p){return p*0.9;}, // VIP
[](double p){return p*0.8;} // Super VIP
};
double getDiscount(UserType type, double price) {
return discountTable[static_cast<int>(type)](price);
}
适用场景:
- 策略数量固定
- 需要C语言兼容接口
7. 实际项目中的经验总结
在我参与的多个C++项目中,策略模式的成功应用离不开以下几点经验:
-
接口设计要稳定:抽象策略接口一旦确定,修改成本很高,前期需要充分设计
-
策略粒度要适中:策略划分太细会导致类爆炸,太粗则失去灵活性
-
文档和示例很重要:为每个策略类提供清晰的文档和使用示例
-
性能监控不可少:特别是虚函数调用频繁的场景
-
测试覆盖所有策略组合:确保策略之间不会产生意外交互
一个常见的陷阱是过度使用策略模式。我曾经见过一个系统把每个小算法都封装成策略,结果导致代码难以理解和维护。策略模式应该用于真正需要灵活变化的算法,而不是所有情况。
