1. 观察者模式:为什么C++开发者需要它?
在C++项目中,我们经常遇到这样的场景:当某个核心对象的状态发生变化时,需要自动通知多个依赖对象进行相应更新。想象一下游戏开发中的成就系统——玩家每获得一个新装备,成就系统、UI界面、存档系统都需要立即响应。如果采用传统的直接调用方式,代码会变成一团乱麻:
cpp复制// 糟糕的紧耦合实现
class Player {
public:
void AddItem(Item item) {
inventory_.push_back(item);
achievementSystem_.CheckItem(item); // 直接调用成就系统
uiManager_.UpdateInventory(); // 直接调用UI
saveSystem_.MarkDirty(); // 直接调用存档系统
}
private:
AchievementSystem achievementSystem_;
UIManager uiManager_;
SaveSystem saveSystem_;
};
这种实现方式存在三个致命缺陷:
- 违反开闭原则:每次新增观察者都要修改Player类
- 强耦合:Player类需要了解所有观察者的细节
- 难以测试:无法单独测试Player类的物品添加逻辑
观察者模式通过解耦解决了这些问题。它的核心思想是:让观察者(Observer)订阅主题(Subject),当主题状态变化时自动通知所有观察者,而不需要知道观察者的具体类型。这就像微信公众号的订阅机制——作者发布新文章时,所有订阅者都会自动收到推送,而作者完全不需要知道订阅者是谁。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. C++观察者模式的经典实现
2.1 基础接口设计
标准的观察者模式包含两个核心接口:
cpp复制// 观察者接口
class IObserver {
public:
virtual ~IObserver() = default;
virtual void Update(const std::string& message) = 0;
};
// 主题接口
class ISubject {
public:
virtual ~ISubject() = default;
virtual void Attach(IObserver* observer) = 0;
virtual void Detach(IObserver* observer) = 0;
virtual void Notify() = 0;
};
2.2 具体主题实现
以游戏中的玩家背包系统为例:
cpp复制class Inventory : public ISubject {
public:
void AddItem(const std::string& item) {
items_.push_back(item);
Notify(); // 物品添加后立即通知所有观察者
}
void Attach(IObserver* observer) override {
observers_.push_back(observer);
}
void Detach(IObserver* observer) override {
observers_.erase(
std::remove(observers_.begin(), observers_.end(), observer),
observers_.end());
}
void Notify() override {
for (auto observer : observers_) {
observer->Update("Inventory changed: new item added");
}
}
private:
std::vector<std::string> items_;
std::vector<IObserver*> observers_;
};
2.3 具体观察者实现
成就系统观察者示例:
cpp复制class AchievementSystem : public IObserver {
public:
AchievementSystem(const std::string& name) : name_(name) {}
void Update(const std::string& message) override {
std::cout << name_ << " received: " << message << std::endl;
// 检查是否解锁了新成就
CheckAchievements();
}
private:
void CheckAchievements() {
// 成就检查逻辑
}
std::string name_;
};
3. 现代C++的改进实现
传统实现存在裸指针管理和线程安全问题。以下是使用现代C++特性的改进版本:
3.1 使用智能指针和弱引用
cpp复制class Subject {
public:
void Attach(std::shared_ptr<IObserver> observer) {
observers_.push_back(std::weak_ptr<IObserver>(observer));
}
void Notify() {
auto it = observers_.begin();
while (it != observers_.end()) {
if (auto observer = it->lock()) {
observer->Update();
++it;
} else {
it = observers_.erase(it);
}
}
}
private:
std::vector<std::weak_ptr<IObserver>> observers_;
};
3.2 类型安全的观察者模式
使用模板避免类型擦除:
cpp复制template <typename T>
class Observer {
public:
virtual ~Observer() = default;
virtual void Update(const T& data) = 0;
};
template <typename T>
class Observable {
public:
void Subscribe(std::shared_ptr<Observer<T>> observer) {
observers_.push_back(std::weak_ptr<Observer<T>>(observer));
}
void Notify(const T& data) {
for (auto& weak_observer : observers_) {
if (auto observer = weak_observer.lock()) {
observer->Update(data);
}
}
}
private:
std::vector<std::weak_ptr<Observer<T>>> observers_;
};
4. 实际应用中的进阶技巧
4.1 事件过滤与优先级
有时观察者只关心特定事件:
cpp复制enum class EventType {
ITEM_ADDED,
ITEM_REMOVED,
PLAYER_LEVEL_UP
};
struct GameEvent {
EventType type;
std::any data;
};
class EventObserver {
public:
virtual void OnEvent(const GameEvent& event) = 0;
};
class EventSystem {
public:
void AddObserver(EventType type, std::shared_ptr<EventObserver> observer) {
observers_[type].push_back(std::weak_ptr<EventObserver>(observer));
}
void Dispatch(const GameEvent& event) {
auto it = observers_.find(event.type);
if (it != observers_.end()) {
for (auto& weak_observer : it->second) {
if (auto observer = weak_observer.lock()) {
observer->OnEvent(event);
}
}
}
}
private:
std::unordered_map<EventType,
std::vector<std::weak_ptr<EventObserver>>> observers_;
};
4.2 线程安全实现
多线程环境下的观察者模式:
cpp复制#include <mutex>
class ThreadSafeSubject {
public:
void Attach(std::shared_ptr<IObserver> observer) {
std::lock_guard<std::mutex> lock(mutex_);
observers_.emplace_back(observer);
}
void Notify() {
std::vector<std::weak_ptr<IObserver>> observers_copy;
{
std::lock_guard<std::mutex> lock(mutex_);
observers_copy = observers_;
}
for (auto& weak_observer : observers_copy) {
if (auto observer = weak_observer.lock()) {
observer->Update();
}
}
}
private:
std::vector<std::weak_ptr<IObserver>> observers_;
std::mutex mutex_;
};
5. 性能优化与常见陷阱
5.1 观察者模式的性能考量
- 内存占用:每个主题维护观察者列表会增加内存开销
- 通知开销:大量观察者时Notify()可能成为瓶颈
- 缓存局部性:观察者分散在内存各处可能导致缓存命中率下降
优化策略:
- 批量通知:积累多个变化后一次性通知
- 分层观察:将观察者分组,只通知相关组
- 使用对象池:重用观察者对象减少内存分配
5.2 常见陷阱及解决方案
陷阱1:观察者持有主题的shared_ptr导致循环引用
cpp复制// 错误示例:循环引用
class BadObserver : public IObserver {
std::shared_ptr<Subject> subject_; // 导致内存泄漏
};
解决方案:观察者只持有主题的weak_ptr
cpp复制class GoodObserver : public IObserver {
std::weak_ptr<Subject> subject_; // 安全
};
陷阱2:在观察者回调中修改观察者列表
cpp复制// 危险代码:在Update中取消订阅
void Update() override {
subject_->Detach(this); // 可能导致迭代器失效
}
解决方案:使用标记延迟删除
cpp复制void Notify() {
// 先收集需要删除的观察者
std::vector<IObserver*> to_remove;
for (auto observer : observers_) {
observer->Update();
if (observer->ShouldDetach()) {
to_remove.push_back(observer);
}
}
// 最后统一删除
for (auto observer : to_remove) {
Detach(observer);
}
}
6. 与其他设计模式的协作
观察者模式常与其他模式结合使用:
6.1 与命令模式结合
cpp复制class Command {
public:
virtual ~Command() = default;
virtual void Execute() = 0;
};
class CommandInvoker : public ISubject {
public:
void ExecuteCommand(std::unique_ptr<Command> cmd) {
cmd->Execute();
Notify(); // 通知观察者命令已执行
}
};
6.2 与中介者模式结合
cpp复制class GameMediator : public ISubject {
public:
void PlayerLevelUp() {
// 协调各系统
achievementSystem_.CheckLevel();
uiSystem_.UpdateLevelDisplay();
saveSystem_.Save();
Notify(); // 通知其他观察者
}
};
6.3 与状态模式结合
cpp复制class PlayerState : public ISubject {
public:
void ChangeState(State newState) {
currentState_ = newState;
Notify(); // 通知观察者状态变化
}
};
7. 实际项目案例:游戏事件系统
在大型游戏项目中,观察者模式通常演化为完整的事件系统:
cpp复制// 事件定义
struct GameEvent {
enum Type {
ENTITY_SPAWNED,
ENTITY_DESTROYED,
PLAYER_HIT
};
Type type;
EntityID entity;
Vec3 position;
// 其他事件数据...
};
// 事件监听器接口
class IEventListener {
public:
virtual ~IEventListener() = default;
virtual void OnEvent(const GameEvent& event) = 0;
};
// 事件总线(全局单例)
class EventBus {
public:
static EventBus& Get() {
static EventBus instance;
return instance;
}
void Subscribe(GameEvent::Type type, IEventListener* listener) {
listeners_[type].push_back(listener);
}
void Unsubscribe(GameEvent::Type type, IEventListener* listener) {
auto& listeners = listeners_[type];
listeners.erase(std::remove(listeners.begin(), listeners.end(), listener),
listeners.end());
}
void Publish(const GameEvent& event) {
for (auto listener : listeners_[event.type]) {
listener->OnEvent(event);
}
}
private:
std::unordered_map<GameEvent::Type,
std::vector<IEventListener*>> listeners_;
};
// 使用示例
class AchievementSystem : public IEventListener {
public:
AchievementSystem() {
EventBus::Get().Subscribe(GameEvent::PLAYER_HIT, this);
}
~AchievementSystem() {
EventBus::Get().Unsubscribe(GameEvent::PLAYER_HIT, this);
}
void OnEvent(const GameEvent& event) override {
if (event.type == GameEvent::PLAYER_HIT) {
// 处理玩家受击事件
}
}
};
8. 测试观察者模式
良好的观察者实现应该易于测试:
8.1 使用Mock观察者
cpp复制class MockObserver : public IObserver {
public:
MOCK_METHOD(void, Update, (const std::string&), (override));
};
TEST(InventoryTest, NotifiesObserversOnAddItem) {
Inventory inventory;
auto observer = std::make_shared<MockObserver>();
// 期望Update被调用一次
EXPECT_CALL(*observer, Update("Inventory changed: new item added"));
inventory.Attach(observer.get());
inventory.AddItem("Sword");
}
8.2 测试线程安全性
cpp复制TEST(ThreadSafeSubjectTest, ConcurrentAccess) {
ThreadSafeSubject subject;
auto observer = std::make_shared<MockObserver>();
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.emplace_back([&] {
subject.Attach(observer);
subject.Notify();
});
}
for (auto& t : threads) {
t.join();
}
// 不应发生数据竞争或崩溃
}
9. 替代方案与模式比较
虽然观察者模式很强大,但并非唯一选择:
9.1 回调函数
cpp复制class Inventory {
public:
using Callback = std::function<void(const std::string&)>;
void SetChangeCallback(Callback cb) {
callback_ = std::move(cb);
}
void AddItem(const std::string& item) {
items_.push_back(item);
if (callback_) {
callback_(item);
}
}
private:
std::vector<std::string> items_;
Callback callback_;
};
优点:
- 实现简单
- 无动态内存分配
缺点:
- 只能注册一个回调
- 不如观察者模式灵活
9.2 信号槽系统(如Boost.Signals2)
cpp复制#include <boost/signals2.hpp>
class Inventory {
public:
boost::signals2::signal<void(const std::string&)> onItemAdded;
void AddItem(const std::string& item) {
items_.push_back(item);
onItemAdded(item);
}
private:
std::vector<std::string> items_;
};
// 使用
Inventory inv;
inv.onItemAdded.connect([](const auto& item) {
std::cout << "Item added: " << item << std::endl;
});
优点:
- 线程安全
- 支持连接管理
- 类型安全
缺点:
- 增加第三方依赖
- 可能过度设计简单场景
10. 最佳实践总结
- 明确生命周期管理:使用智能指针避免内存泄漏,特别注意循环引用问题
- 考虑线程安全:多线程环境下必须保护观察者列表
- 控制通知粒度:太频繁的通知会影响性能,太少则失去实时性
- 避免在通知中修改观察者列表:使用标记延迟删除模式
- 提供取消订阅机制:允许观察者在不再需要通知时注销
- 考虑性能影响:对于高频事件,考虑批量通知或异步通知
- 保持接口简洁:观察者接口应该尽可能通用,避免过度特化
- 文档化订阅关系:特别是隐式的订阅关系,避免难以追踪的依赖
在最近的一个RPG游戏项目中,我们重构了成就系统使用观察者模式后,发现以下经验值得分享:
- 将成就条件分类为不同事件类型,显著减少了不必要的检查
- 使用弱引用避免了成就系统与玩家系统的循环依赖
- 异步事件队列解决了高频事件(如位置更新)的性能问题
- 添加事件统计分析帮助我们识别了不必要的事件订阅
