1. 项目概述:为什么我们需要观察者模式?
在C++项目里,尤其是那些涉及复杂UI交互、游戏事件系统或者需要解耦业务逻辑的场景,你是不是经常遇到这样的麻烦:一个对象的状态改变了,得手动去通知一堆其他对象更新,代码里到处都是if (condition) { updateA(); updateB(); ... },牵一发而动全身,维护起来头大如斗。这就是典型的“紧耦合”问题,各个模块像一团乱麻缠在一起。
观察者模式就是为了解决这个痛点而生的。它是一种行为设计模式,核心思想是定义了一种一对多的依赖关系。当一个对象(我们称之为“主题”或“被观察者”)的状态发生改变时,所有依赖于它的对象(我们称之为“观察者”)都会自动得到通知并被更新。你可以把它想象成微信公众号的订阅机制:你(观察者)关注了某个公众号(主题)。公众号一发布新文章(状态改变),所有订阅者(观察者列表)的手机就会自动收到推送通知(更新回调),而公众号完全不需要知道具体是谁订阅了它。
对于C++开发者,尤其是面临面试或构建中大型项目的朋友,深入理解并手写实现观察者模式,绝不仅仅是背下一个设计模式的名字。它能让你从根本上理解如何设计松耦合、高内聚的系统架构,写出更灵活、更易扩展的代码。无论是游戏里的成就系统、UI控件的数据绑定,还是分布式系统中的事件总线,其底层思想都离不开观察者模式。接下来,我们就从思想到代码,彻底拆解这个模式,让你不仅能写出标准实现,更能理解其变种和实战中的精妙之处。
2. 模式思想深度拆解:不仅仅是“订阅-发布”
2.1 核心角色与职责
要理解观察者模式,首先得搞清楚戏台上的几个关键角色:
Subject(主题/被观察者):
- 职责:维护一个观察者对象的列表。提供“添加”、“删除”和“通知”观察者的接口。
- 关键点:它只知道有一群观察者在盯着它,但完全不知道这些观察者具体是谁、是干什么的。它只负责在自身状态变化时,遍历列表,调用每个观察者的某个约定好的方法(比如
update)。
Observer(观察者):
- 职责:定义一个更新接口(通常是纯虚函数),用于接收来自主题的通知。
- 关键点:所有具体的观察者都必须实现这个接口。当主题通知时,观察者通过这个接口获取更新,并执行自己的业务逻辑。观察者可以订阅多个不同的主题。
ConcreteSubject(具体主题):
- 职责:继承或实现
Subject。它拥有实际的状态,当这些状态改变时,调用继承的“通知”方法。 - 关键点:它是触发通知的源头。通常,我们会在它的状态设置函数(
setState)里加入通知逻辑。
- 职责:继承或实现
ConcreteObserver(具体观察者):
- 职责:实现
Observer接口。保存一个指向ConcreteSubject的引用(或指针),以便在收到通知时,能获取主题的具体状态。 - 关键点:它实现了具体的响应行为。比如,一个UI标签观察者,在收到通知后会去读取主题的最新数据并更新显示。
- 职责:实现
注意:这里有一个经典的设计抉择——是采用“推”模型还是“拉”模型?在“推”模型中,主题在通知时会将被改变的数据作为参数传递给观察者。在“拉”模型中,主题只发送一个简单的通知,观察者收到通知后,主动去主题那里“拉取”所需的数据。C++实现中,两者都很常见,
推模型更直接,拉模型则更灵活(观察者按需索取),我们后续的代码会展示拉模型,因为它更符合松耦合的精神。
2.2 模式的价值与适用场景
理解了角色,我们再来看看这个模式到底好在哪里,以及什么时候该用它:
- 价值一:解耦:这是最大的好处。主题和观察者之间依赖于抽象(
Subject和Observer接口),而非具体实现。你可以独立地复用或修改主题或观察者,只要接口不变,另一边就无需改动。 - 价值二:支持广播通信:主题不需要指定接收者,通知会自动发给所有观察者。这在事件处理系统中非常有用。
- 价值三:符合开闭原则:你可以随时增加新的观察者类,而无需修改主题的代码。系统扩展变得非常容易。
那么,什么场景下你应该考虑使用观察者模式呢?
- GUI事件处理:按钮点击、鼠标移动等事件,监听这些事件的控件就是观察者。
- 数据模型与视图分离:MVC/MVVM架构中,当模型(主题)数据变化时,多个视图(观察者)需要自动更新。
- 游戏开发:成就系统(观察者监听玩家击杀、收集等事件)、AI系统(AI监听玩家位置变化)。
- 分布式系统的消息中间件:生产者(主题)发布消息,多个消费者(观察者)订阅并处理。
- 监控与日志系统:被监控的服务(主题)状态变化,触发多个日志记录器、报警器(观察者)工作。
3. 基础代码实现:从零搭建一个松耦合系统
理论说再多,不如一行代码。我们来实现一个经典的例子:一个气象站(WeatherStation作为具体主题)负责收集温度、湿度数据。多个显示设备(如CurrentConditionsDisplay当前状况显示板、StatisticsDisplay统计显示板作为具体观察者)订阅气象站的数据更新。当气象站数据变化时,所有显示板自动更新。
3.1 定义观察者与主题接口
这是模式的基石,定义了通信的契约。
// Observer.h - 观察者接口 #ifndef OBSERVER_H #define OBSERVER_H // 前向声明,避免头文件循环依赖。Observer只需要知道Subject存在,不需要知道其细节。 class Subject; class Observer { public: virtual ~Observer() = default; // 基类虚析构函数,确保派生类对象能被正确释放 // 更新接口。当主题状态改变时,主题会调用此方法。 // 参数subject指向状态发生改变的主题对象,观察者可以据此查询主题状态。 virtual void update(Subject* subject) = 0; }; #endif // OBSERVER_H// Subject.h - 主题接口 #ifndef SUBJECT_H #define SUBJECT_H #include <vector> #include <memory> // 用于std::shared_ptr #include “Observer.h” // 需要知道Observer类 class Subject { public: virtual ~Subject() = default; // 注册观察者 virtual void registerObserver(std::shared_ptr<Observer> observer) = 0; // 移除观察者 virtual void removeObserver(std::shared_ptr<Observer> observer) = 0; // 通知所有观察者 virtual void notifyObservers() = 0; }; #endif // SUBJECT_H实操心得:这里使用了
std::shared_ptr<Observer>来管理观察者的生命周期。这是一个现代C++的推荐做法,可以避免手动内存管理的麻烦和悬空指针的风险。主题持有观察者的智能指针,当最后一个持有该观察者的主题被销毁时,观察者对象也会被自动清理。当然,如果观察者的生命周期由其他模块严格管理,使用原始指针或std::weak_ptr也是可行的,但shared_ptr在大多数情况下是最省心的选择。
3.2 实现具体主题:气象站
具体主题需要维护状态和观察者列表,并在状态改变时触发通知。
// WeatherStation.h #ifndef WEATHER_STATION_H #define WEATHER_STATION_H #include “Subject.h” #include <memory> #include <vector> #include <algorithm> // 用于std::remove class WeatherStation : public Subject { private: std::vector<std::shared_ptr<Observer>> observers_; // 观察者列表 float temperature_; float humidity_; float pressure_; public: WeatherStation() : temperature_(0.0f), humidity_(0.0f), pressure_(1013.25f) {} // 实现Subject接口 void registerObserver(std::shared_ptr<Observer> observer) override { observers_.push_back(observer); } void removeObserver(std::shared_ptr<Observer> observer) override { // 使用erase-remove惯用法来移除元素 observers_.erase( std::remove(observers_.begin(), observers_.end(), observer), observers_.end() ); } void notifyObservers() override { for (auto& observer : observers_) { if (auto obs = observer.lock()) { // 使用weak_ptr时需检查,shared_ptr则直接调用 obs->update(this); // “拉”模型:将自身指针传给观察者 } } } // 设置气象数据,并在设置后自动通知观察者 void setMeasurements(float temperature, float humidity, float pressure) { temperature_ = temperature; humidity_ = humidity; pressure_ = pressure; measurementsChanged(); // 数据改变,触发更新流程 } // 供观察者“拉取”数据的接口 float getTemperature() const { return temperature_; } float getHumidity() const { return humidity_; } float getPressure() const { return pressure_; } private: // 封装通知逻辑,确保数据设置后必然通知 void measurementsChanged() { notifyObservers(); } }; #endif // WEATHER_STATION_H3.3 实现具体观察者:显示板
观察者需要在构造函数中订阅主题,并在update方法中做出响应。
// CurrentConditionsDisplay.h #ifndef CURRENT_CONDITIONS_DISPLAY_H #define CURRENT_CONDITIONS_DISPLAY_H #include “Observer.h” #include “WeatherStation.h” // 需要知道具体主题以获取数据 #include <iostream> class CurrentConditionsDisplay : public Observer { private: // 持有对主题的引用(这里用指针),用于“拉取”数据 WeatherStation* weatherStation_; float temperature_; float humidity_; public: // 构造函数中注册自己到主题 explicit CurrentConditionsDisplay(WeatherStation* station) : weatherStation_(station), temperature_(0.0f), humidity_(0.0f) { if (weatherStation_) { // 注意:这里需要将this指针转换为shared_ptr。 // 在实际项目中,观察者对象本身通常也由智能指针管理。 // 这里为了示例简单,假设Display对象生命周期由外部管理。 // 更安全的做法是让Display也继承std::enable_shared_from_this。 weatherStation_->registerObserver(std::shared_ptr<Observer>(this)); } } ~CurrentConditionsDisplay() override { if (weatherStation_) { // 析构时取消注册,防止主题通知一个已销毁的对象 weatherStation_->removeObserver(std::shared_ptr<Observer>(this)); } } void update(Subject* subject) override { // 安全转换,确保通知来自我们关心的主题 if (auto ws = dynamic_cast<WeatherStation*>(subject)) { temperature_ = ws->getTemperature(); humidity_ = ws->getHumidity(); display(); } } void display() const { std::cout << “[当前状况显示] 温度: “ << temperature_ << “°C, 湿度: “ << humidity_ << “%” << std::endl; } }; #endif // CURRENT_CONDITIONS_DISPLAY_H// StatisticsDisplay.h - 另一个观察者示例 #ifndef STATISTICS_DISPLAY_H #define STATISTICS_DISPLAY_H #include “Observer.h” #include “WeatherStation.h” #include <iostream> #include <vector> #include <numeric> // for std::accumulate class StatisticsDisplay : public Observer { private: WeatherStation* weatherStation_; std::vector<float> tempHistory_; float maxTemp_ = -100.0f; float minTemp_ = 100.0f; float avgTemp_ = 0.0f; public: explicit StatisticsDisplay(WeatherStation* station) : weatherStation_(station) { if (weatherStation_) { weatherStation_->registerObserver(std::shared_ptr<Observer>(this)); } } ~StatisticsDisplay() override { if (weatherStation_) { weatherStation_->removeObserver(std::shared_ptr<Observer>(this)); } } void update(Subject* subject) override { if (auto ws = dynamic_cast<WeatherStation*>(subject)) { float currentTemp = ws->getTemperature(); tempHistory_.push_back(currentTemp); // 更新统计值 maxTemp_ = std::max(maxTemp_, currentTemp); minTemp_ = std::min(minTemp_, currentTemp); avgTemp_ = std::accumulate(tempHistory_.begin(), tempHistory_.end(), 0.0f) / tempHistory_.size(); display(); } } void display() const { std::cout << “[统计显示] 平均/最高/最低温度: “ << avgTemp_ << “°C / “ << maxTemp_ << “°C / “ << minTemp_ << “°C” << std::endl; } }; #endif // STATISTICS_DISPLAY_H3.4 组装与运行:体验松耦合的魅力
最后,我们写一个简单的main函数来验证整个系统的工作。
// main.cpp #include “WeatherStation.h” #include “CurrentConditionsDisplay.h” #include “StatisticsDisplay.h” #include <memory> int main() { // 1. 创建主题(气象站) WeatherStation weatherStation; // 2. 创建观察者(显示板),并传入主题进行注册 CurrentConditionsDisplay currentDisplay(&weatherStation); StatisticsDisplay statsDisplay(&weatherStation); std::cout << “=== 第一次数据更新 ===” << std::endl; // 3. 主题数据更新,自动通知所有观察者 weatherStation.setMeasurements(25.0f, 65.0f, 1012.0f); std::cout << “\n=== 第二次数据更新 ===” << std::endl; weatherStation.setMeasurements(26.5f, 70.0f, 1011.5f); std::cout << “\n=== 第三次数据更新 ===” << std::endl; weatherStation.setMeasurements(24.0f, 90.0f, 1013.0f); // 4. 观察者会在每次setMeasurements后自动显示新数据 return 0; }运行这个程序,你会看到每次调用weatherStation.setMeasurements后,两个显示板都会自动打印出最新的信息,而气象站完全不知道显示板是如何工作的。这就是观察者模式实现的松耦合通信。
4. 进阶实现与关键问题剖析
上面的基础实现已经揭示了模式的核心,但在实际项目中,直接这样用可能会踩坑。我们来深入几个关键问题,并给出更健壮的解决方案。
4.1 内存管理与生命周期陷阱
这是C++实现观察者模式最容易出问题的地方。在上面的示例中,我们在CurrentConditionsDisplay的构造函数里用std::shared_ptr<Observer>(this)注册了自己。这非常危险!因为它创建了一个新的、独立的shared_ptr,与可能管理该对象生命周期的外部shared_ptr不共享控制块。这会导致双重删除或内存泄漏。
解决方案一:使用std::enable_shared_from_this
这是标准库提供的安全获取对象自身shared_ptr的工具。
// CurrentConditionsDisplay.h (改进版) #include <memory> class CurrentConditionsDisplay : public Observer, public std::enable_shared_from_this<CurrentConditionsDisplay> { // 关键继承 public: // 工厂函数,确保对象总是被shared_ptr管理 static std::shared_ptr<CurrentConditionsDisplay> create(WeatherStation* station) { // 不能直接在构造函数中使用shared_from_this,所以用工厂函数 auto ptr = std::shared_ptr<CurrentConditionsDisplay>(new CurrentConditionsDisplay(station)); // 注册时使用正确的shared_ptr if (station) { station->registerObserver(ptr); } return ptr; } // ... 其他成员函数,update, display等 ... private: // 构造函数设为私有,强制使用工厂函数 explicit CurrentConditionsDisplay(WeatherStation* station) : weatherStation_(station) {} WeatherStation* weatherStation_; // ... };解决方案二:主题持有std::weak_ptr<Observer>
让主题持有观察者的弱引用,可以避免影响观察者的生命周期,并安全地处理观察者已失效的情况。
// Subject.h (改进版) #include <vector> #include <memory> class Observer; // 前向声明 class Subject { public: virtual ~Subject() = default; virtual void registerObserver(std::weak_ptr<Observer> observer) = 0; // 使用weak_ptr virtual void removeObserver(std::weak_ptr<Observer> observer) = 0; virtual void notifyObservers() = 0; protected: std::vector<std::weak_ptr<Observer>> observers_; // 存储weak_ptr }; // WeatherStation.cpp 中 notifyObservers 的实现 void WeatherStation::notifyObservers() { auto it = observers_.begin(); while (it != observers_.end()) { if (auto obs = it->lock()) { // 尝试提升为shared_ptr obs->update(this); ++it; } else { // 观察者对象已不存在,从列表中移除失效的weak_ptr it = observers_.erase(it); } } }注意事项:
weak_ptr方案更安全,但增加了lock()检查的开销。在实际高频通知的场景下,需要权衡性能。通常,在观察者数量不多或通知不频繁时,weak_ptr是首选。
4.2 线程安全考虑
如果主题和观察者可能在不同的线程中被访问和修改(例如,一个线程更新传感器数据,另一个线程处理UI更新),那么我们的实现就不是线程安全的。observers_向量可能在被遍历时被另一个线程修改,导致崩溃。
简易的线程安全改造:
// WeatherStation.h (线程安全版) #include <mutex> class WeatherStation : public Subject { private: mutable std::mutex mutex_; // 互斥锁,保护共享数据 // ... 其他成员 ... public: void registerObserver(std::weak_ptr<Observer> observer) override { std::lock_guard<std::mutex> lock(mutex_); observers_.push_back(observer); } void removeObserver(std::weak_ptr<Observer> observer) override { std::lock_guard<std::mutex> lock(mutex_); // 移除逻辑需要处理weak_ptr的比较,略复杂,通常需要自定义查找 // 一种方法是存储shared_ptr,但用weak_ptr通知,这里简化处理 auto it = std::find_if(observers_.begin(), observers_.end(), [&observer](const std::weak_ptr<Observer>& wp) { return !wp.owner_before(observer) && !observer.owner_before(wp); }); if (it != observers_.end()) { observers_.erase(it); } } void notifyObservers() override { std::vector<std::weak_ptr<Observer>> observersCopy; { std::lock_guard<std::mutex> lock(mutex_); observersCopy = observers_; // 复制列表,缩短锁持有时间 } for (auto& weakObs : observersCopy) { if (auto obs = weakObs.lock()) { obs->update(this); // 注意:update方法本身也应该是线程安全的 } } } // ... };实操心得:这里采用了“复制后通知”的策略,在
notifyObservers中先复制观察者列表,然后释放锁,再遍历复制的列表进行通知。这样做的好处是避免了在持有锁的情况下调用未知的update方法,从而减少死锁风险,并提高了通知过程的并发性。但代价是每次通知都需要复制一次列表。对于观察者数量巨大或通知极其频繁的场景,需要更精细的锁策略或无锁数据结构。
4.3 性能优化:避免不必要的通知
有时主题的多个状态可能同时改变,或者某些状态的改变对某些观察者没有意义。频繁地、无差别地通知所有观察者会造成性能浪费。
优化策略一:按事件类型通知
为不同的事件定义枚举或标识,观察者订阅特定的事件,主题只通知对该事件感兴趣的观察者。
enum class WeatherEvent { TemperatureChanged, HumidityChanged, PressureChanged, AllChanged }; class Observer { public: virtual void update(Subject* subject, WeatherEvent event) = 0; // 增加事件参数 }; class Subject { public: virtual void registerObserver(std::weak_ptr<Observer> observer, WeatherEvent event) = 0; // 需要维护一个 map<WeatherEvent, vector<weak_ptr<Observer>>> };优化策略二:脏标记与延迟通知
主题设置一个“脏”标志位。当状态改变时,只标记为“脏”,而不立即通知。在一个统一的更新循环(比如游戏的主循环)中,检查所有主题的脏标记,如果为“脏”,则进行批量通知,然后清除标记。这可以将多次连续的状态变更合并为一次通知。
class WeatherStation : public Subject { private: bool dataChanged_ = false; // ... public: void setMeasurements(float t, float h, float p) { temperature_ = t; humidity_ = h; pressure_ = p; dataChanged_ = true; // 只标记,不通知 } // 由外部调度器调用 void notifyIfChanged() { if (dataChanged_) { notifyObservers(); dataChanged_ = false; } } };5. 模式变体与现代C++实践
观察者模式有很多“变种”,在现代C++项目中也常常以不同的面貌出现。
5.1 使用std::function与信号槽
这是将观察者模式“轻量化”和“现代化”的常用手法。主题不再维护抽象的Observer对象列表,而是维护一个std::function回调列表。观察者可以将任何可调用对象(函数、lambda表达式、成员函数指针绑定的对象)注册为槽。
#include <functional> #include <vector> class WeatherStation { public: using Callback = std::function<void(float temp, float humidity, float pressure)>; void registerCallback(const Callback& cb) { callbacks_.push_back(cb); } void setMeasurements(float t, float h, float p) { temperature_ = t; humidity_ = h; pressure_ = p; for (const auto& cb : callbacks_) { cb(temperature_, humidity_, pressure_); // “推”模型 } } private: std::vector<Callback> callbacks_; float temperature_, humidity_, pressure_; }; // 使用示例 int main() { WeatherStation ws; // 使用lambda注册观察者 auto displayCallback = [](float t, float h, float p) { std::cout << “Lambda显示: Temp=“ << t << std::endl; }; ws.registerCallback(displayCallback); // 使用std::bind注册成员函数 // 假设有一个Display类 // ws.registerCallback(std::bind(&Display::update, &myDisplay, std::placeholders::_1, ...)); ws.setMeasurements(20.0f, 50.0f, 1010.0f); return 0; }这种方式极其灵活,省去了定义继承体系的麻烦,特别适合回调逻辑简单、生命周期明确的场景。许多GUI框架(如Qt的信号槽)和事件库的核心思想与此类似。
5.2 观察者模式与发布-订阅模式
很多人会混淆观察者模式和发布-订阅模式。它们确实相似,但有一个关键区别:耦合度。
- 观察者模式:主题和观察者彼此知晓。观察者直接向主题注册。这是一种直接的点对点通信,可以看作是“紧耦合”的发布-订阅(虽然比硬编码通知要松)。
- 发布-订阅模式:发布者和订阅者完全不知道对方的存在。它们通过一个中间件(事件总线、消息代理)进行通信。发布者向某个频道发布消息,订阅者订阅感兴趣的频道。中间件负责路由消息。这是更彻底的解耦。
在C++中,你可以实现一个简单的事件总线作为发布-订阅模型的核心:
#include <map> #include <vector> #include <functional> #include <string> #include <memory> class EventBus { using Callback = std::function<void(const std::string&, const void*)>; // 事件名,数据指针 std::map<std::string, std::vector<Callback>> subscribers_; public: void subscribe(const std::string& eventName, Callback cb) { subscribers_[eventName].push_back(cb); } void publish(const std::string& eventName, const void* eventData = nullptr) { auto it = subscribers_.find(eventName); if (it != subscribers_.end()) { for (const auto& cb : it->second) { cb(eventName, eventData); } } } }; // 任何模块都可以是发布者或订阅者,它们只依赖EventBus,不互相依赖。6. 实战避坑指南与经验总结
结合我多年的项目经验,在C++中使用观察者模式,以下几点至关重要:
生命周期管理是第一要务:这是C++资源管理的核心。优先考虑使用智能指针(
shared_ptr/weak_ptr)来管理观察者关系。如果使用原始指针,必须建立清晰的“谁创建,谁销毁”或“所有者”规则,并在析构函数中确保取消注册。忘记在观察者析构时取消注册是导致崩溃的常见原因。警惕通知过程中的修改:在主题的
notifyObservers方法中遍历观察者列表时,如果某个观察者的update方法内部又调用了主题的registerObserver或removeObserver,可能会使正在迭代的容器失效(如迭代器失效)。解决方案可以是先复制列表再通知,或者使用标识位延迟处理注册/注销请求。考虑线程安全:在多线程环境下,对观察者列表的增删改查都必须加锁。同时,通知操作本身也可能需要锁,但要小心死锁。通常建议像前面示例一样,复制列表后释放锁再执行回调。
避免过长的调用链与循环依赖:观察者的
update方法应尽量轻量、快速,不要执行耗时操作或产生新的通知,否则可能导致通知链过长甚至无限递归。也要小心观察者A通知B,B又通知A这种循环依赖。性能与扩展性的权衡:如果观察者数量非常多(成千上万),线性遍历列表进行通知可能成为瓶颈。此时可以考虑按事件类型分组订阅,或者使用更高效的数据结构。对于极度性能敏感的场景,可能需要寻求观察者模式之外的解决方案。
接口设计要稳定:
Subject和Observer的接口一旦确定,应尽量保持稳定。因为修改接口会影响所有实现类。如果后续需要传递更多信息,可以考虑使用一个包含事件数据的上下文对象作为参数,而不是修改函数签名。
观察者模式是一个强大的工具,它能显著降低模块间的耦合度。在C++中实现它,需要你仔细处理内存、线程和性能这些底层细节。从经典的双接口继承实现,到现代基于std::function的回调,再到引入中间件的发布-订阅,其核心思想一脉相承。理解其本质,并根据项目具体需求(代码规模、性能要求、团队习惯)选择最合适的实现变体,是一个优秀C++工程师的必备能力。下次当你发现代码中到处都是对象间的直接调用时,不妨想想,是不是该引入一个“观察者”来梳理一下关系了。