1. 观察者模式与策略模式的核心价值
在C++开发中,设计模式的选择直接影响着代码的可维护性和扩展性。观察者模式(Observer Pattern)和策略模式(Strategy Pattern)是两种最常用的行为型模式,它们分别解决了不同维度的设计问题。
观察者模式的核心在于建立对象间的一对多依赖关系,当一个对象状态改变时,所有依赖它的对象都会自动收到通知。这种模式特别适合GUI事件处理、实时数据监控等场景。比如在股票交易系统中,当股价变动时需要立即通知多个显示终端更新数据。
策略模式则定义了算法家族,将每个算法封装起来,使它们可以互相替换。这种模式让算法的变化独立于使用算法的客户端,典型的应用场景包括支付方式选择、压缩算法切换等。在电商平台中,不同的促销策略可以随时切换而不影响订单处理流程。
这两种模式在C++中的实现有其特殊性。由于C++没有内置的观察者接口,我们需要手动实现订阅/发布机制。而策略模式在C++中可以利用模板和函数对象获得更好的性能。理解它们的实现差异对编写高效C++代码至关重要。
2. C++观察者模式的实现细节
2.1 基础实现框架
在C++中实现观察者模式通常需要三个核心组件:
cpp复制// 观察者接口
class Observer {
public:
virtual ~Observer() = default;
virtual void update(const std::string& message) = 0;
};
// 被观察者基类
class Subject {
private:
std::vector<std::weak_ptr<Observer>> observers_;
public:
void attach(const std::shared_ptr<Observer>& observer) {
observers_.emplace_back(observer);
}
void notifyAll(const std::string& message) {
for (auto it = observers_.begin(); it != observers_.end(); ) {
if (auto observer = it->lock()) {
observer->update(message);
++it;
} else {
it = observers_.erase(it);
}
}
}
};
// 具体被观察者
class ConcreteSubject : public Subject {
public:
void changeState(int newState) {
state_ = newState;
notifyAll("State changed to " + std::to_string(state_));
}
private:
int state_ = 0;
};
这个基础实现有几个关键点需要注意:
- 使用weak_ptr避免循环引用导致的内存泄漏
- 通知时自动清理已失效的观察者
- 虚析构函数确保派生类正确释放资源
2.2 线程安全改进
在实际项目中,观察者模式往往需要处理多线程环境。以下是线程安全的改进版本:
cpp复制#include <mutex>
class ThreadSafeSubject : public Subject {
public:
void attach(const std::shared_ptr<Observer>& observer) override {
std::lock_guard<std::mutex> lock(mutex_);
Subject::attach(observer);
}
void notifyAll(const std::string& message) override {
std::lock_guard<std::mutex> lock(mutex_);
Subject::notifyAll(message);
}
private:
std::mutex mutex_;
};
注意:简单的互斥锁可能引发死锁,特别是当观察者的update方法又回调Subject时。更安全的做法是:
- 在notifyAll中先复制观察者列表
- 然后在锁外执行通知
- 或者使用递归锁
2.3 性能优化技巧
对于高频事件通知场景,可以考虑以下优化:
- 批量通知:积累多个变化后一次性通知
cpp复制void batchNotify() {
if (!dirty_) return;
dirty_ = false;
notifyAll("Batch update");
}
- 差异化通知:只通知关注特定事件的观察者
cpp复制void notify(const std::string& eventType, const std::string& message) {
for (auto& observer : getObserversFor(eventType)) {
observer->update(message);
}
}
- 无锁队列:使用原子操作实现高性能事件队列
3. 策略模式在C++中的高级应用
3.1 传统实现与模板实现对比
传统策略模式实现:
cpp复制class SortStrategy {
public:
virtual void sort(std::vector<int>& data) = 0;
};
class QuickSort : public SortStrategy {
void sort(std::vector<int>& data) override { /*...*/ }
};
class Context {
std::unique_ptr<SortStrategy> strategy_;
public:
void setStrategy(std::unique_ptr<SortStrategy> strategy) {
strategy_ = std::move(strategy);
}
void execute(std::vector<int>& data) {
strategy_->sort(data);
}
};
模板策略模式实现:
cpp复制template<typename Strategy>
class Context {
Strategy strategy_;
public:
void execute(std::vector<int>& data) {
strategy_.sort(data);
}
};
struct QuickSort {
void sort(std::vector<int>& data) { /*...*/ }
};
两种实现的对比:
| 特性 | 传统实现 | 模板实现 |
|---|---|---|
| 运行时灵活性 | 高(可动态切换) | 低(编译时确定) |
| 性能开销 | 虚函数调用开销 | 无额外开销 |
| 代码膨胀 | 低 | 可能较高 |
| 二进制兼容性 | 好 | 差 |
3.2 策略模式与函数对象的结合
现代C++更推荐使用函数对象(Functor)或std::function实现策略模式:
cpp复制using SortStrategy = std::function<void(std::vector<int>&)>;
class Context {
SortStrategy strategy_;
public:
void setStrategy(SortStrategy strategy) {
strategy_ = std::move(strategy);
}
void execute(std::vector<int>& data) {
strategy_(data);
}
};
// 使用lambda表达式
context.setStrategy([](std::vector<int>& data) {
std::sort(data.begin(), data.end());
});
这种方式的优势:
- 无需定义接口类
- 可以使用lambda表达式快速定义策略
- 兼容普通函数、成员函数、函数对象等
3.3 策略模式的缓存优化
当策略对象创建成本较高时,可以考虑使用对象池或缓存:
cpp复制class StrategyFactory {
std::unordered_map<std::string, std::shared_ptr<SortStrategy>> cache_;
public:
std::shared_ptr<SortStrategy> get(const std::string& type) {
auto it = cache_.find(type);
if (it != cache_.end()) return it->second;
std::shared_ptr<SortStrategy> strategy;
if (type == "quick") strategy = std::make_shared<QuickSort>();
else if (type == "merge") strategy = std::make_shared<MergeSort>();
if (strategy) cache_[type] = strategy;
return strategy;
}
};
4. 模式组合实战案例:游戏AI系统
4.1 需求分析与设计
假设我们要开发一个游戏AI系统,需求如下:
- NPC需要根据玩家行为改变状态
- 不同状态下NPC有不同的行为策略
- 需要支持动态添加新的状态和策略
这个场景完美结合了观察者模式和策略模式:
- 观察者模式:玩家行为触发NPC状态变化
- 策略模式:不同状态对应不同行为策略
4.2 核心实现代码
cpp复制// 事件类型
enum class EventType { Attack, Heal, Move };
// 观察者接口
class AIObserver {
public:
virtual void onEvent(EventType type, const Position& pos) = 0;
};
// 策略接口
class BehaviorStrategy {
public:
virtual void execute(NPC& npc) = 0;
};
// NPC类
class NPC : public AIObserver {
std::unique_ptr<BehaviorStrategy> strategy_;
State currentState_;
public:
void onEvent(EventType type, const Position& pos) override {
currentState_ = determineNewState(type, pos);
strategy_ = BehaviorFactory::create(currentState_);
}
void update() {
strategy_->execute(*this);
}
};
// 具体策略
class AggressiveStrategy : public BehaviorStrategy {
void execute(NPC& npc) override {
// 攻击最近的玩家
}
};
class DefensiveStrategy : public BehaviorStrategy {
void execute(NPC& npc) override {
// 寻找掩体
}
};
4.3 性能优化实践
在游戏开发中,性能至关重要。我们可以采用以下优化:
- 内存池:预分配策略对象,避免频繁内存分配
cpp复制class StrategyPool {
std::array<AggressiveStrategy, 100> aggressivePool_;
std::array<DefensiveStrategy, 100> defensivePool_;
// ...
};
- 数据导向设计:将策略需要的数据集中存储
cpp复制struct NPCData {
Position pos;
Health health;
// ...
};
class BehaviorSystem {
void update(std::span<NPCData> npcs,
std::span<BehaviorStrategy*> strategies) {
// 批量处理
}
};
- SIMD优化:对某些策略使用向量化指令
cpp复制class FlockingStrategy : public BehaviorStrategy {
void execute(NPC& npc) override {
// 使用SIMD指令计算群体行为
}
};
5. 常见问题与调试技巧
5.1 观察者模式的内存泄漏
典型症状:程序运行时间越长,内存占用越高。
排查步骤:
- 使用Valgrind或AddressSanitizer检查内存错误
- 确认所有Subject在析构时清除了observers_
- 检查观察者是否被正确释放
解决方案:
cpp复制virtual ~Subject() {
// 确保通知时不会访问已释放的观察者
observers_.clear();
}
5.2 策略模式的性能瓶颈
典型症状:策略切换时出现卡顿。
优化方法:
- 预加载常用策略
- 使用flyweight模式共享策略状态
- 考虑无锁数据结构
5.3 多线程环境下的竞态条件
调试技巧:
- 使用ThreadSanitizer检测数据竞争
- 对策略的共享状态加锁
- 考虑将策略设计为无状态的
cpp复制class StatelessStrategy {
void execute(NPC& npc, const Context& ctx) {
// 所有状态通过ctx参数传入
}
};
5.4 模式选择的经验法则
根据我的项目经验,可以参考以下决策树:
- 需要动态响应状态变化 → 观察者模式
- 需要在运行时切换算法 → 策略模式
- 两者都需要 → 组合使用
- 性能关键路径 → 考虑模板策略
- 需要最大灵活性 → 运行时策略
在实际项目中,我经常遇到需要权衡灵活性和性能的情况。一个实用的技巧是先用运行时策略模式实现功能,待性能测试确认热点后再用模板策略优化关键路径。