C++观察者模式:从原理到现代实现,解决对象间松耦合通知
1. 项目概述为什么我们需要观察者模式如果你写过一些稍微有点规模的C项目尤其是涉及到UI更新、事件响应或者数据监控大概率会遇到一个头疼的问题一个对象的状态变了怎么让其他一堆对象都知道并且自动更新最直接的办法就是让状态变化的对象去“硬编码”调用所有关心它的对象的更新方法。比如一个气象站的数据中心WeatherData温度变了它得自己去调用CurrentConditionsDisplay的update方法再调用StatisticsDisplay的update方法还得调用ForecastDisplay的update方法……每增加一个显示面板就得去修改WeatherData的代码。这种紧耦合的设计维护起来简直是噩梦违反了“对修改关闭对扩展开放”的开闭原则。观察者模式Observer Pattern就是为了优雅地解决这个问题而生的。它定义了一种一对多的依赖关系当一个对象称为“主题”或“可观察者”的状态发生改变时所有依赖于它的对象称为“观察者”都会自动得到通知并更新。这就像你订阅了一个公众号主题公众号每次发新文章状态改变所有订阅者观察者都会收到推送通知。在C中实现它不仅能让你深刻理解这种松耦合的设计思想更是处理事件驱动系统、模型-视图分离架构的利器。无论是游戏引擎里的成就系统监听玩家状态还是Qt框架中的信号与槽机制其核心思想都脱胎于观察者模式。接下来我将结合《Head First设计模式》中的经典气象站案例带你从零开始用现代C我会用到C11/14的一些特性让实现更安全、更优雅实现一个类型安全、避免内存泄漏的观察者模式。我们不止步于“能跑通”更要深究“为什么这么设计”以及在实际项目中你会踩到的那些坑。2. 核心思路与接口设计建立松耦合的契约观察者模式的核心在于解耦。主题不应该知道观察者具体是谁、具体做了什么它只关心有一群观察者实现了某个统一的接口。同样观察者也只关心主题提供的那个获取状态的接口。这种通过抽象接口建立的契约是松耦合的基石。2.1 定义观察者接口首先我们定义观察者接口。这是一个纯虚基类任何想成为观察者的类都必须继承它并实现update方法。// Observer.h #ifndef OBSERVER_H #define OBSERVER_H // 前向声明避免头文件循环包含。Subject的具体实现细节Observer接口并不需要知道。 class Subject; class Observer { public: virtual ~Observer() default; // 虚析构函数确保通过基类指针删除派生类对象时正确释放资源。 /** * 当主题状态改变时主题会调用此方法来通知所有观察者。 * 这是观察者模式的“推”模型Push Model主题将更新数据推送给观察者。 * 参数 subject 是一个指向主题的指针观察者可以通过它来回拉Pull更多具体数据。 * 在实际项目中你也可以设计为“拉”模型即update()无参数观察者自己持有主题的引用去拉数据。 */ virtual void update(Subject* subject) 0; }; #endif // OBSERVER_H设计要点与避坑指南虚析构函数是必须的这是C多态性的基石。如果基类的析构函数不是虚函数那么通过基类指针删除派生类对象时只会调用基类的析构函数导致派生类的部分资源泄漏。 default让编译器生成默认实现简洁安全。使用前向声明在Observer的头文件中我们只用了class Subject;而不是#include Subject.h。这减少了编译依赖加快了编译速度也避免了可能的头文件循环包含问题。只要在Observer.cpp如果有的话或者观察者具体实现的.cpp文件中包含Subject.h即可。“推”模型 vs “拉”模型我们这里采用的是经典的“推”模型主题将自身指针传递给观察者。观察者可以决定是否需要、以及需要哪些数据。另一种是“拉”模型update()函数无参观察者内部持有主题的引用需要时主动去“拉”数据。“推”模型更主动但可能传递不必要的数据“拉”模型更被动但观察者需要知道如何获取数据。我们选择“推”模型因为它更符合《Head First》的示例且灵活性足够。2.2 定义主题接口接下来定义主题即可观察者接口。主题需要提供注册、移除和通知观察者的能力。// Subject.h #ifndef SUBJECT_H #define SUBJECT_H #include memory #include vector class Observer; // 前向声明 class Subject { public: virtual ~Subject() default; /** * 注册一个观察者。从此这个观察者将接收状态变更通知。 * 参数 observer 是一个指向观察者的智能指针使用智能指针可以自动管理生命周期避免裸指针带来的内存泄漏风险。 */ virtual void registerObserver(std::shared_ptrObserver observer) 0; /** * 移除一个观察者。被移除后该观察者将不再接收通知。 */ virtual void removeObserver(std::shared_ptrObserver observer) 0; /** * 通知所有已注册的观察者。当主题的内部状态发生改变时调用此方法。 */ virtual void notifyObservers() 0; }; #endif // SUBJECT_H设计要点与避坑指南使用智能指针管理观察者生命周期这里使用了std::shared_ptrObserver。这是现代C工程实践的关键一步。如果使用裸指针Observer*主题很难知道何时该删除观察者指针极易造成内存泄漏或悬垂指针。使用shared_ptr主题和观察者的使用者共享所有权只有当最后一个shared_ptr被销毁时观察者对象才会被释放。这大大简化了内存管理。接口简洁主题接口只关心三件事增、删、通知。它不关心观察者具体是谁也不关心自己具体是什么状态。这保持了接口的纯粹性和通用性。notifyObservers的调用时机这个方法应该由具体主题在状态确实发生改变后调用。通常是在主题的setter方法内部。不要在getter或者不改变状态的方法里调用否则会导致不必要的、甚至无限循环的通知。3. 具体实现从抽象接口到气象站案例有了抽象的接口我们就可以实现具体的主题和观察者了。我们以《Head First》中的WeatherData气象数据为主题实现几个具体的显示面板作为观察者。3.1 实现具体主题WeatherDataWeatherData是核心它负责测量、存储气象数据温度、湿度、气压并在数据变化时通知所有观察者。// WeatherData.h #ifndef WEATHERDATA_H #define WEATHERDATA_H #include Subject.h #include Observer.h #include memory #include vector #include algorithm // 用于std::find class WeatherData : public Subject { public: WeatherData() : temperature_(0.0f), humidity_(0.0f), pressure_(1013.25f) {} // 实现Subject接口 void registerObserver(std::shared_ptrObserver observer) override { observers_.push_back(observer); } void removeObserver(std::shared_ptrObserver observer) override { // 使用std::remove配合erase来移除元素。注意remove不会真的删除只是把要删除的元素移到末尾。 auto it std::find(observers_.begin(), observers_.end(), observer); if (it ! observers_.end()) { observers_.erase(it); } // 更现代、更安全的写法是使用std::erase (C20) 或 std::erase_if // 例如 C20: std::erase(observers_, observer); } void notifyObservers() override { // 遍历所有观察者调用其update方法并传递this指针即WeatherData对象自身 for (const auto observer : observers_) { // 注意这里传递的是基类Subject的指针但由于WeatherData继承自Subject // 在观察者的update函数中可以通过dynamic_cast或static_cast如果确定类型转换回WeatherData*来访问具体数据。 observer-update(this); } } // 供观察者拉取数据的公共接口 float getTemperature() const { return temperature_; } float getHumidity() const { return humidity_; } float getPressure() const { return pressure_; } // 模拟气象站测量到新数据这是触发状态改变和通知的入口 void setMeasurements(float temperature, float humidity, float pressure) { // 在实际项目中这里可能涉及复杂的传感器读取或网络请求 temperature_ temperature; humidity_ humidity; pressure_ pressure; // 关键步骤数据更新后立即通知所有观察者 measurementsChanged(); } private: // 当测量数据改变时被调用的私有方法 void measurementsChanged() { notifyObservers(); } // 气象数据成员变量 float temperature_; float humidity_; float pressure_; // 观察者列表。使用shared_ptr容器存储主题不独占观察者所有权。 std::vectorstd::shared_ptrObserver observers_; }; #endif // WEATHERDATA_H实操心得与陷阱removeObserver的实现细节直接使用vector::erase并传入迭代器是正确做法。早期的错误示范是直接在for循环中边遍历边删除这会导致迭代器失效。我们这里先find再erase。在C20中可以使用std::erase代码更简洁。通知的时机setMeasurements是改变状态的唯一公开入口。在它内部我们更新私有数据后调用一个私有方法measurementsChanged()再由这个方法去调用notifyObservers()。这样设计的好处是将来如果通知的逻辑需要改变比如增加日志、条件判断只需要修改measurementsChanged()一处。这符合“将变化的部分封装起来”的设计原则。数据传递在notifyObservers()中我们传递this一个Subject*给观察者。观察者需要将其转换回WeatherData*来获取具体数据。这里存在一个向下转型downcast的问题。我们稍后在观察者实现中讨论如何安全地处理。3.2 实现具体观察者显示面板我们实现三个显示面板当前状况、统计信息和天气预报。它们都继承自Observer接口。3.2.1 当前状况显示面板// CurrentConditionsDisplay.h #ifndef CURRENT_CONDITIONS_DISPLAY_H #define CURRENT_CONDITIONS_DISPLAY_H #include Observer.h #include WeatherData.h // 这里需要包含具体主题的头文件因为要调用其getter方法 #include iostream #include memory class CurrentConditionsDisplay : public Observer { public: // 构造函数中注册自己到主题。使用weak_ptr避免循环引用。 explicit CurrentConditionsDisplay(std::shared_ptrWeatherData weatherData) : weatherData_(weatherData), temperature_(0.0f), humidity_(0.0f) { weatherData_-registerObserver(std::shared_ptrObserver(this)); // 注意这里将this转为shared_ptr有风险 } ~CurrentConditionsDisplay() override { // 析构时从主题中移除自己避免主题通知一个已销毁的对象。 auto data weatherData_.lock(); // 尝试提升为shared_ptr if (data) { >// Observer.h (修正版) #ifndef OBSERVER_H #define OBSERVER_H #include memory class Subject; class Observer : public std::enable_shared_from_thisObserver { public: virtual ~Observer() default; virtual void update(Subject* subject) 0; // 提供一个安全的获取自身shared_ptr的方法 std::shared_ptrObserver getPtr() { return shared_from_this(); } }; #endif // OBSERVER_H然后我们修改CurrentConditionsDisplay的创建和使用方式。关键点观察者对象必须从一开始就由shared_ptr管理。// CurrentConditionsDisplay.h (修正版核心改动) class CurrentConditionsDisplay : public Observer { public: // 使用静态工厂函数来创建对象确保对象始终被shared_ptr管理。 static std::shared_ptrCurrentConditionsDisplay create(std::shared_ptrWeatherData weatherData) { // 使用make_shared一次性分配对象和控制块效率更高且安全。 auto display std::make_sharedCurrentConditionsDisplay(weatherData); // 现在可以安全地注册了因为display已经是一个合法的shared_ptr。 weatherData-registerObserver(display); return display; } // 将构造函数设为private强制用户使用工厂函数。 private: explicit CurrentConditionsDisplay(std::shared_ptrWeatherData weatherData) : weatherData_(weatherData), temperature_(0.0f), humidity_(0.0f) { // 构造函数里不再注册注册移到工厂函数中。 } public: // 析构函数中移除自己 ~CurrentConditionsDisplay() override { auto data weatherData_.lock(); if (data) { // 这里可以使用从enable_shared_from_this继承来的shared_from_this() >// StatisticsDisplay.h #ifndef STATISTICS_DISPLAY_H #define STATISTICS_DISPLAY_H #include Observer.h #include WeatherData.h #include iostream #include vector #include algorithm #include memory class StatisticsDisplay : public Observer { public: static std::shared_ptrStatisticsDisplay create(std::shared_ptrWeatherData weatherData) { auto display std::make_sharedStatisticsDisplay(weatherData); weatherData-registerObserver(display); return display; } ~StatisticsDisplay() override { auto data weatherData_.lock(); if (data) { >// ForecastDisplay.h #ifndef FORECAST_DISPLAY_H #define FORECAST_DISPLAY_H #include Observer.h #include WeatherData.h #include iostream #include memory class ForecastDisplay : public Observer { public: static std::shared_ptrForecastDisplay create(std::shared_ptrWeatherData weatherData) { auto display std::make_sharedForecastDisplay(weatherData); weatherData-registerObserver(display); return display; } ~ForecastDisplay() override { auto data weatherData_.lock(); if (data) { >// main.cpp #include WeatherData.h #include CurrentConditionsDisplay.h #include StatisticsDisplay.h #include ForecastDisplay.h #include memory #include thread #include chrono int main() { // 1. 创建主题气象站 auto weatherData std::make_sharedWeatherData(); // 2. 创建观察者显示面板并通过工厂函数自动注册 auto currentDisplay CurrentConditionsDisplay::create(weatherData); auto statisticsDisplay StatisticsDisplay::create(weatherData); auto forecastDisplay ForecastDisplay::create(weatherData); std::cout 模拟气象数据更新 std::endl; // 3. 模拟气象数据变化 weatherData-setMeasurements(25.0f, 65.0f, 1015.0f); std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟1秒间隔 weatherData-setMeasurements(26.5f, 70.0f, 1012.0f); std::this_thread::sleep_for(std::chrono::seconds(1)); weatherData-setMeasurements(23.0f, 90.0f, 1018.0f); std::this_thread::sleep_for(std::chrono::seconds(1)); // 4. 动态移除一个观察者 std::cout \n 移除当前状况显示面板 std::endl; // 我们不需要手动调用remove因为currentDisplay析构时会自动调用。 // 但为了演示我们可以手动将其重置释放触发其析构。 currentDisplay.reset(); // currentDisplay智能指针释放资源对象被销毁析构函数中从主题移除自己。 std::cout 当前状况显示面板已移除。 std::endl; // 5. 再次更新数据只有剩下的两个面板会收到通知 weatherData-setMeasurements(21.0f, 85.0f, 1020.0f); // 6. 程序结束所有智能指针超出作用域自动清理内存。 // weatherData, statisticsDisplay, forecastDisplay 依次销毁。 // statisticsDisplay 和 forecastDisplay 的析构函数会将自己从 weatherData 的列表中移除。 // 最后 weatherData 被销毁。 return 0; }编译并运行这个程序假设你使用gg -stdc14 -o weather_station main.cpp WeatherData.cpp CurrentConditionsDisplay.cpp StatisticsDisplay.cpp ForecastDisplay.cpp ./weather_station你应该能看到类似以下的输出 模拟气象数据更新 Current conditions: 25°C and 65% humidity Avg/Max/Min temperature: 25/25/25°C Forecast: Improving weather on the way! Current conditions: 26.5°C and 70% humidity Avg/Max/Min temperature: 25.75/26.5/25°C Forecast: Watch out for cooler, rainy weather. Current conditions: 23°C and 90% humidity Avg/Max/Min temperature: 24.8333/26.5/23°C Forecast: Improving weather on the way! 移除当前状况显示面板 当前状况显示面板已移除。 Avg/Max/Min temperature: 24.375/26.5/21°C Forecast: Improving weather on the way!完美观察者模式成功运行。当数据更新时所有注册的显示面板都自动更新了。当我们移除“当前状况显示”后后续的通知它就不再接收。5. 深入探讨模式变体、陷阱与进阶优化我们的基础实现已经可用但在实际生产环境中还需要考虑更多细节。5.1 线程安全上面的实现是非线程安全的。如果registerObserver、removeObserver、notifyObservers可能被多个线程同时调用或者notifyObservers中观察者的update方法修改了观察者列表就会导致数据竞争Data Race或迭代器失效。解决方案 最简单的办法是使用互斥锁std::mutex保护观察者列表。但要注意死锁问题特别是在notifyObservers中调用观察者的update时观察者又可能回调主题的方法比如在update中调用removeObserver。// WeatherData.h (线程安全粗略版) #include mutex #include shared_mutex // C17 读写锁更好 class WeatherData : public Subject { public: void registerObserver(std::shared_ptrObserver observer) override { std::lock_guardstd::mutex lock(mutex_); observers_.push_back(observer); } void removeObserver(std::shared_ptrObserver observer) override { std::lock_guardstd::mutex lock(mutex_); auto it std::find(observers_.begin(), observers_.end(), observer); if (it ! observers_.end()) { observers_.erase(it); } } void notifyObservers() override { std::vectorstd::shared_ptrObserver observers_copy; { // 复制一份观察者列表减少锁的持有时间。 std::lock_guardstd::mutex lock(mutex_); observers_copy observers_; } for (const auto observer : observers_copy) { observer-update(this); // 注意update函数本身也应该是线程安全的 } } private: mutable std::mutex mutex_; // ... 其他成员 };重要提示在notifyObservers中复制列表是一个常用技巧它避免了在遍历列表时持有锁防止了观察者update方法内操作列表导致的死锁。但复制本身有性能开销。另一种方法是使用读写锁std::shared_mutex注册/移除时写锁通知时读锁。5.2 通知顺序与性能顺序问题std::vector保证了插入顺序但通知顺序就是注册顺序。有时你可能需要优先级。可以考虑使用std::multiset或std::priority_queue来存储带优先级的观察者。性能问题如果观察者数量非常多成千上万notifyObservers的遍历会成为瓶颈。可以考虑异步通知将通知任务提交到线程池避免阻塞主题线程。但异步会带来新的复杂度观察者接收通知的顺序不确定且需要处理线程间数据同步。5.3 使用 std::function 与信号槽的现代实现如果你不需要完整的观察者模式类层次C11的std::function和std::bind或lambda可以提供一种更轻量、更灵活的“信号-槽”机制。#include functional #include vector class SimpleWeatherData { public: using UpdateCallback std::functionvoid(float temp, float humidity, float pressure); void registerCallback(const UpdateCallback cb) { callbacks_.push_back(cb); } void setMeasurements(float t, float h, float p) { temp_ t; humi_ h; pres_ p; for (const auto cb : callbacks_) { cb(temp_, humi_, pres_); // 直接调用函数对象 } } private: float temp_, humi_, pres_; std::vectorUpdateCallback callbacks_; }; // 使用lambda注册 SimpleWeatherData swd; swd.registerCallback([](float t, float h, float p) { std::cout Lambda: t std::endl; });这种方式更简洁类型安全通过模板可以支持任意签名但失去了将观察者抽象为对象的能力更适合简单的回调场景。Qt框架的信号槽、Boost.Signals2库都是这种思想的强大实现。5.4 主题基类是否需要模板化我们的Subject接口使用了Observer基类。这意味着任何观察者都必须继承自Observer。有时我们希望主题能通知任意类型的回调。这时可以使用模板。templatetypename... Args class Subject { public: using ObserverType std::functionvoid(Args...); void registerObserver(ObserverType obs) { /*...*/ } void notifyObservers(Args... args) { for(auto obs : observers_) { obs(args...); } } private: std::vectorObserverType observers_; }; // 具体主题 class AdvancedWeatherData : public Subjectfloat, float, float { // ... };模板化提供了极大的灵活性但代价是失去了运行时的动态类型安全和统一的观察者基类。你需要根据项目需求权衡。6. 总结与个人心得观察者模式在C中的实现核心挑战往往不在于模式本身的理解而在于C特有的对象生命周期管理和资源安全。通过这个完整的实现我希望你不仅学会了模式更掌握了以下关键点智能指针是救星始终优先使用std::shared_ptr和std::weak_ptr来管理涉及多个所有者主题和观察者的对象生命周期。裸指针在现代C项目中是万恶之源。警惕循环引用主题和观察者互相持有对方的shared_ptr会导致内存泄漏。用weak_ptr指向你需要观察但不应拥有所有权的对象。enable_shared_from_this的必要性当一个对象需要获取指向自身的shared_ptr时比如在析构函数中从主题列表移除自己必须让这个类继承自std::enable_shared_from_this并且该对象必须从一开始就被shared_ptr管理通常通过工厂函数和make_shared实现。接口设计关乎扩展性“推”还是“拉”传递什么数据这些决定会影响未来代码的扩展。我们的实现采用了“推”模型主题指针让观察者按需拉取是一个平衡的选择。现实世界的复杂性线程安全、通知性能、动态类型转换dynamic_cast的安全性这些都是理论示例中常常忽略但实际项目必须面对的。在简单的单线程程序里我们的基础实现足够用一旦涉及并发就必须引入同步机制。最后设计模式不是银弹观察者模式也不例外。它引入了间接性调试起来可能更费劲。但对于需要解耦事件源和事件处理器的场景——比如GUI框架、游戏引擎、分布式系统中的事件总线——观察者模式提供的松耦合优势是无可替代的。理解其原理并在C的语境下安全、高效地实现它是你迈向资深C工程师的重要一步。下次当你需要让一个对象的变化通知到其他对象时不妨想想这个气象站的例子。