90、STL与观察者模式:事件通知、订阅机制、松耦合
观察者模式,说白了就是「你关注我,我变了就告诉你」。
我在做游戏引擎的时候,经常遇到这种场景:玩家血量变了,UI要更新、音效要播放、成就系统要检查。如果每个模块都直接去轮询血量,代码会乱成一锅粥。观察者模式就是解决这个问题的——它让对象之间保持松耦合,一个对象状态变了,所有依赖它的对象都能自动收到通知。
观察者模式的核心思想
观察者模式有两个核心角色:
- 主题(Subject):维护一个观察者列表,状态变化时通知所有观察者。
- 观察者(Observer):注册到主题上,收到通知后执行自己的逻辑。
你想想看,这就像公众号和订阅者。公众号发文章,所有订阅者都能收到。公众号不需要知道订阅者是谁,订阅者也不需要关心公众号怎么运作。这就是松耦合。
关键点:观察者模式的核心是「推」而不是「拉」。主题主动推送事件,观察者被动接收。这避免了轮询带来的性能浪费和代码耦合。
STL如何助力观察者模式
STL虽然没有直接提供观察者模式的类,但它的容器和算法可以帮我们优雅地实现这个模式。我个人习惯用 std::vector 或 std::list 来存储观察者,用 std::function 来封装回调函数。
为什么用 std::function?因为它可以接受函数指针、lambda表达式、函数对象,灵活性极高。我在项目中遇到过需要同时支持普通函数和成员函数的情况,std::function 一把搞定。
一个完整的事件通知系统
下面我写一个简单但完整的事件通知系统。假设我们有一个「玩家血量变化」的事件,多个模块需要响应。
#include <iostream>
#include <vector>
#include <functional>
#include <string>
#include <algorithm>
// 事件参数结构体
struct HealthEvent {
int oldHealth;
int newHealth;
std::string playerName;
};
// 主题类
class HealthSubject {
private:
// 观察者列表:存储回调函数
std::vector<std::function<void(const HealthEvent&)>> observers;
public:
// 订阅事件
void subscribe(std::function<void(const HealthEvent&)> callback) {
observers.push_back(callback);
}
// 取消订阅
void unsubscribe(const std::function<void(const HealthEvent&)>& callback) {
// 这里用 remove-erase 惯用法
auto it = std::remove_if(observers.begin(), observers.end(),
[&](const std::function<void(const HealthEvent&)>& obs) {
// 注意:std::function 不支持直接比较
// 实际项目中可以用 token 或 ID 来标识
return false; // 简化处理
});
// observers.erase(it, observers.end());
}
// 通知所有观察者
void notify(const HealthEvent& event) {
for (auto& observer : observers) {
observer(event);
}
}
};
// 观察者:UI模块
class UIModule {
public:
void onHealthChanged(const HealthEvent& event) {
std::cout << "[UI] 血量从 " << event.oldHealth
<< " 变为 " << event.newHealth << std::endl;
if (event.newHealth < 20) {
std::cout << "[UI] 警告:血量过低!" << std::endl;
}
}
};
// 观察者:音效模块
class AudioModule {
public:
void onHealthChanged(const HealthEvent& event) {
if (event.newHealth < event.oldHealth) {
std::cout << "[Audio] 播放受伤音效" << std::endl;
} else {
std::cout << "[Audio] 播放治疗音效" << std::endl;
}
}
};
int main() {
HealthSubject subject;
UIModule ui;
AudioModule audio;
// 使用 lambda 绑定成员函数
subject.subscribe([&ui](const HealthEvent& e) {
ui.onHealthChanged(e);
});
subject.subscribe([&audio](const HealthEvent& e) {
audio.onHealthChanged(e);
});
// 模拟血量变化
HealthEvent event1{100, 80, "Player1"};
subject.notify(event1);
std::cout << "--- 第二次变化 ---" << std::endl;
HealthEvent event2{80, 15, "Player1"};
subject.notify(event2);
return 0;
}
运行结果:
[UI] 血量从 100 变为 80
[Audio] 播放受伤音效
--- 第二次变化 ---
[UI] 血量从 80 变为 15
[UI] 警告:血量过低!
[Audio] 播放受伤音效
小技巧:用 lambda 绑定成员函数时,注意捕获 this 指针或对象的引用。如果对象可能被销毁,记得用 std::weak_ptr 来避免悬空指针。
观察者模式的核心流程图
下面这张图展示了整个事件通知的流程:
流程很简单:观察者先通过 subscribe() 注册到主题上。当主题状态变化时,调用 notify() 遍历观察者列表,逐个调用回调函数。每个观察者收到事件后,执行自己的逻辑。
松耦合的好处
观察者模式最大的价值就是松耦合。主题不需要知道观察者的具体类型,观察者也不需要知道主题的内部实现。它们只通过事件接口通信。
我曾经在一个项目中接手过一段代码,里面全是硬编码的调用关系:ui->update()、audio->play()、achievement->check() 写在一起。后来需求变了,要加一个日志模块,得改好几个文件。改成观察者模式后,新增模块只需要订阅事件,一行代码都不用改原来的逻辑。
避坑指南:我曾经遇到过一个问题——观察者回调中又触发了事件,导致无限递归。比如在血量变化回调中又修改了血量。解决方案是加一个「正在通知」的标志位,或者在事件队列中延迟处理。
进阶:带优先级的事件系统
有时候我们希望某些观察者先收到通知。比如日志模块应该最后执行,UI模块应该优先更新。可以用 std::multimap 来存储带优先级的观察者:
#include <map>
#include <functional>
class PrioritySubject {
private:
// 用 multimap 存储:key 是优先级,value 是回调
std::multimap<int, std::function<void(const HealthEvent&)>> observers;
public:
void subscribe(int priority, std::function<void(const HealthEvent&)> callback) {
observers.insert({priority, callback});
}
void notify(const HealthEvent& event) {
// multimap 默认按 key 升序排列
for (auto& [priority, callback] : observers) {
callback(event);
}
}
};
这样,优先级高的观察者(数值小)会先收到通知。我在做UI框架时就用过这个技巧,让渲染模块先更新,日志模块最后记录。
总结
观察者模式是解耦的利器,STL 的 std::function 和容器让实现变得异常简单。记住几个要点:
- 用
std::vector或std::list存储回调 - 用
std::function封装任意可调用对象 - 注意避免循环触发和悬空指针
- 需要优先级时用
std::multimap
嗯,这个模式在实际项目中太常用了。你想想看,从游戏引擎到GUI框架,从消息总线到插件系统,到处都有它的影子。掌握了它,你的代码会变得更灵活、更易维护。
核心要点:观察者模式 = 事件驱动 + 松耦合 + 自动通知。STL 提供了基础设施,你只需要搭好架子。
公众号:蓝海资料掘金营,微信deep3321