1. 项目概述从气象站到代码的桥梁最近在重构一个老项目的数据推送模块看着里面各种if-else和硬编码的回调函数头疼得不行。这让我想起了几年前用C写的一个小型气象数据监控系统当时为了解耦数据源和多个显示终端果断上了观察者模式。今天就把这个“气象站实战”的里里外外拆开揉碎了讲清楚这不仅是设计模式的学习更是解决“一对多”依赖问题的经典思路。简单说观察者模式要解决的核心问题是当一个对象比如气象站的状态发生改变时如何自动通知所有依赖它的对象比如手机App、网页大屏、数据库记录器并且让这些对象能各自做出响应而气象站本身完全不用关心通知了谁、对方具体干了什么。这就像微信群里发了个公告所有群成员都能立刻收到但有人选择阅读有人选择保存还有人选择转发发公告的人并不需要知道这些细节。在C里实现这个模式你会深刻体会到面向对象设计中“针对接口编程而非实现编程”和“松耦合”两大原则的魅力。下面我们就从最核心的设计思路开始一步步搭建一个可运行、可扩展的气象站Demo。2. 核心设计思路与模式解析2.1 为什么是观察者模式假设我们要构建的系统叫WeatherStation气象站它核心的工作是监测温度、湿度和气压。我们需要将这三项数据实时展示到至少三个地方一个当前状况显示屏CurrentConditionsDisplay、一个气象统计显示屏StatisticsDisplay和一个简单的预警装置ForecastDisplay。最直接的写法可能是这样class WeatherStation { private: float temperature_; float humidity_; float pressure_; CurrentConditionsDisplay* current_display_; StatisticsDisplay* stats_display_; ForecastDisplay* forecast_display_; // ... 可能还有更多Display public: void measurementsChanged() { float temp getTemperature(); float humidity getHumidity(); float pressure getPressure(); // 硬编码调用每一个显示器的更新方法 current_display_-update(temp, humidity, pressure); stats_display_-update(temp, humidity, pressure); forecast_display_-update(temp, humidity, pressure); // ... 每增加一个显示器就要在这里加一行代码 } };这种写法的问题显而易见紧耦合WeatherStation类直接依赖于具体的显示器类。一旦要增加或删除一个显示器就必须修改WeatherStation的源代码违反了“开闭原则”。难以维护所有通知逻辑都堆在一个方法里显示器越多这个方法越臃肿。运行时动态性差无法在程序运行时动态地添加或移除观察者。观察者模式通过引入“主题”Subject和“观察者”Observer两个抽象角色来解决这些问题。主题负责维护一个观察者列表并在状态改变时遍历这个列表去通知每一个观察者。观察者则实现一个统一的更新接口。这样主题和具体的观察者之间就只剩下抽象的依赖关系。2.2 模式中的角色与C映射在气象站这个例子里角色映射如下主题Subject对应我们的WeatherData类或WeatherStation的核心数据部分。它负责管理注册、删除观察者并在数据更新时通知它们。观察者Observer对应各种显示屏如CurrentConditionsDisplay。它们实现一个统一的接口以便主题能以统一的方式通知它们。具体主题ConcreteSubject就是WeatherData类的具体实现。它持有实际的状态温度、湿度、气压当这些状态改变时调用通知方法。具体观察者ConcreteObserver就是各个显示屏的具体实现。它们注册到主题上并在收到通知后从主题那里“拉取”或接收“推送”来的数据更新自己的显示。这种结构带来的最大好处是WeatherData类之后可以这样写void WeatherData::measurementsChanged() { notifyObservers(); // 只需要调用这一个方法所有注册的观察者都会被通知到。 }至于notifyObservers()内部怎么实现以及观察者列表如何管理就是我们接下来要深入的核心。3. 核心细节解析与C实现要点3.1 接口设计松耦合的关键接口是解耦的基石。在C中我们通常用纯虚类即抽象基类来定义接口。1. 观察者接口 (Observer)这个接口极其简单只声明一个更新方法。关键在于这个方法的参数设计主要有两种流派“推”模型和“拉”模型。// “推”模型主题将数据直接推送给观察者 class Observer { public: virtual ~Observer() default; // 基类虚析构函数必不可少 virtual void update(float temperature, float humidity, float pressure) 0; }; // “拉”模型主题将自己作为参数传递给观察者观察者按需拉取数据 // class Observer { // public: // virtual ~Observer() default; // virtual void update(const Subject subject) 0; // Subject是主题接口 // };在气象站的例子里数据字段明确且固定温度、湿度、气压使用“推”模型更直观。如果观察者需要的数据差异很大或者主题数据结构复杂“拉”模型能避免推送不必要的数据更灵活。我们本次采用“推”模型。2. 主题接口 (Subject)主题接口需要提供注册、移除和通知观察者的能力。class Subject { public: virtual ~Subject() default; virtual void registerObserver(Observer* o) 0; virtual void removeObserver(Observer* o) 0; virtual void notifyObservers() 0; };注意这里registerObserver和removeObserver接收的是Observer*原始指针。这在简单的示例中没问题但在实际项目中这引入了对象生命周期管理的难题。主题必须非常小心不能通知一个已经被销毁的观察者悬垂指针否则会导致未定义行为崩溃。更健壮的做法是使用std::weak_ptrObserver但这要求观察者本身由std::shared_ptr管理。为了示例清晰我们先使用原始指针但会在后面的“注意事项”中重点讨论这个问题。3.2 具体主题的实现WeatherDataWeatherData类需要实现Subject接口并管理气象数据和观察者列表。#include vector #include algorithm class WeatherData : public Subject { private: std::vectorObserver* observers_; // 观察者列表 float temperature_; float humidity_; float pressure_; public: WeatherData() : temperature_(0.0f), humidity_(0.0f), pressure_(1013.25f) {} // 实现Subject接口 void registerObserver(Observer* o) override { observers_.push_back(o); } void removeObserver(Observer* o) override { // 使用STL算法移除指定指针。注意这要求同一个观察者不要重复注册。 auto it std::find(observers_.begin(), observers_.end(), o); if (it ! observers_.end()) { observers_.erase(it); } } void notifyObservers() override { // 遍历所有观察者调用其update方法推送最新数据 for (Observer* observer : observers_) { observer-update(temperature_, humidity_, pressure_); } } // 气象站自身的业务方法 void setMeasurements(float temperature, float humidity, float pressure) { this-temperature_ temperature; this-humidity_ humidity; this-pressure_ pressure; measurementsChanged(); // 数据设置好后触发通知 } // 其他getter方法供“拉”模型或外部使用 float getTemperature() const { return temperature_; } float getHumidity() const { return humidity_; } float getPressure() const { return pressure_; } private: void measurementsChanged() { notifyObservers(); // 核心状态改变通知所有观察者 } };关键点解析observers_使用std::vectorObserver*存储这是最简单的容器选择。如果对移除操作性能要求高可以考虑std::list。removeObserver中使用了std::find来定位要删除的观察者指针。这里隐含了一个约定同一个观察者对象指针不应该被重复注册到列表中否则remove只会移除第一个找到的实例。setMeasurements是改变主题状态的外部入口。一旦调用它内部会调用measurementsChanged()进而触发notifyObservers()。这是典型的“状态改变-通知”流程。notifyObservers()中的遍历顺序就是观察者被通知的顺序。有时这个顺序很重要需要根据业务需求来设计注册或通知逻辑。3.3 具体观察者的实现各式显示屏观察者需要实现Observer接口并在构造时通常需要订阅注册到主题在析构时取消订阅。1. 当前状况显示屏#include iostream class CurrentConditionsDisplay : public Observer { private: float temperature_; float humidity_; // 通常需要持有主题的引用或指针以便在update时拉取数据如果用拉模型或用于取消注册。 Subject* weather_data_; public: // 构造函数中完成注册 explicit CurrentConditionsDisplay(Subject* weather_data) : weather_data_(weather_data), temperature_(0.0f), humidity_(0.0f) { weather_data_-registerObserver(this); } // 析构函数中完成移除防止主题通知一个已销毁的对象 ~CurrentConditionsDisplay() override { if (weather_data_) { weather_data_-removeObserver(this); } } // 实现Observer接口 void update(float temperature, float humidity, float pressure) override { this-temperature_ temperature; this-humidity_ humidity; display(); } void display() const { std::cout 当前状况: 温度 temperature_ °C, 湿度 humidity_ % std::endl; } };2. 气象统计显示屏这个显示器需要记录历史数据并计算统计值比如平均温度。#include vector class StatisticsDisplay : public Observer { private: std::vectorfloat temperature_history_; Subject* weather_data_; float max_temp_; float min_temp_; float avg_temp_; public: explicit StatisticsDisplay(Subject* weather_data) : weather_data_(weather_data), max_temp_(-200.0f), min_temp_(200.0f), avg_temp_(0.0f) { weather_data_-registerObserver(this); } ~StatisticsDisplay() override { if (weather_data_) { weather_data_-removeObserver(this); } } void update(float temperature, float humidity, float pressure) override { temperature_history_.push_back(temperature); // 更新统计值 if (temperature max_temp_) max_temp_ temperature; if (temperature min_temp_) min_temp_ temperature; float sum 0.0f; for (float temp : temperature_history_) { sum temp; } avg_temp_ sum / temperature_history_.size(); display(); } void display() const { std::cout 温度统计: 最高 max_temp_ °C, 最低 min_temp_ °C, 平均 avg_temp_ °C std::endl; } };3. 简单预警显示屏这个显示器根据气压变化做一个非常简单的“天气预报”。class ForecastDisplay : public Observer { private: float last_pressure_; float current_pressure_; Subject* weather_data_; public: explicit ForecastDisplay(Subject* weather_data) : weather_data_(weather_data), last_pressure_(1013.25f), current_pressure_(1013.25f) { weather_data_-registerObserver(this); } ~ForecastDisplay() override { if (weather_data_) { weather_data_-removeObserver(this); } } void update(float temperature, float humidity, float pressure) override { last_pressure_ current_pressure_; current_pressure_ pressure; display(); } void display() const { std::cout 天气预报: ; if (current_pressure_ last_pressure_) { std::cout 天气可能转好; } else if (current_pressure_ last_pressure_) { std::cout 注意可能下雨或刮风。; } else { std::cout 天气情况基本不变。; } std::cout std::endl; } };4. 完整实战流程与代码整合现在我们把所有部件组装起来看看这个气象站系统如何工作。4.1 主程序模拟气象数据更新#include iostream #include memory // 为了使用std::unique_ptr管理动态对象 int main() { // 1. 创建主题气象数据中心 WeatherData weather_data; // 2. 创建观察者各种显示屏并让它们订阅主题 // 使用智能指针管理避免手动delete的麻烦和内存泄漏风险。 auto current_display std::make_uniqueCurrentConditionsDisplay(weather_data); auto stats_display std::make_uniqueStatisticsDisplay(weather_data); auto forecast_display std::make_uniqueForecastDisplay(weather_data); std::cout 模拟气象站数据更新 std::endl; // 3. 模拟第一次数据更新 std::cout \n[更新1] 设置数据: 温度25.0, 湿度65, 气压1015 std::endl; weather_data.setMeasurements(25.0f, 65.0f, 1015.0f); // 输出 // 当前状况: 温度 25°C, 湿度 65% // 温度统计: 最高 25°C, 最低 25°C, 平均 25°C // 天气预报: 天气可能转好 // 4. 模拟第二次数据更新 std::cout \n[更新2] 设置数据: 温度26.5, 湿度70, 气压1013 std::endl; weather_data.setMeasurements(26.5f, 70.0f, 1013.0f); // 输出 // 当前状况: 温度 26.5°C, 湿度 70% // 温度统计: 最高 26.5°C, 最低 25°C, 平均 25.75°C // 天气预报: 注意可能下雨或刮风。 // 5. 模拟移除一个观察者例如关闭统计显示屏 std::cout \n[操作] 移除统计显示屏观察者... std::endl; // 由于StatisticsDisplay在析构时会自动调用removeObserver我们直接销毁它即可。 stats_display.reset(); // 释放stats_display指向的对象触发其析构函数 // 6. 模拟第三次数据更新此时只有两个观察者会收到通知 std::cout \n[更新3] 设置数据: 温度24.0, 湿度90, 气压1010 std::endl; weather_data.setMeasurements(24.0f, 90.0f, 1010.0f); // 输出 // 当前状况: 温度 24°C, 湿度 90% // 天气预报: 注意可能下雨或刮风。 // 不再有温度统计输出 // main函数结束current_display和forecast_display也会自动析构并取消注册。 return 0; }4.2 编译与运行你可以将上述所有类的定义和实现放在一个或多个.hpp和.cpp文件中。一个简单的单文件编译命令如下假设文件名为weather_station.cpp# 使用g编译需要支持C11或更高标准因为使用了std::make_uniqueC14引入 g -stdc14 -o weather_station weather_station.cpp ./weather_station运行后你将看到清晰的输出展示了数据更新如何自动触发所有注册的显示屏更新以及移除观察者后通知列表的变化。5. 进阶议题与性能考量5.1 线程安全当观察者模式遇上多线程我们上面的实现是非线程安全的。想象一下这个场景一个线程正在遍历observers_向量进行通知notifyObservers而另一个线程同时注册或移除一个观察者修改了observers_向量这会导致迭代器失效引发崩溃如std::vector的重新分配。解决方案互斥锁Mutex在registerObserver、removeObserver和notifyObservers方法内部使用std::mutex进行同步。这是最直接的方法。#include mutex class WeatherData : public Subject { private: mutable std::mutex mtx_; // mutable允许在const成员函数中加锁 std::vectorObserver* observers_; // ... 其他成员 public: void registerObserver(Observer* o) override { std::lock_guardstd::mutex lock(mtx_); observers_.push_back(o); } void notifyObservers() override { std::vectorObserver* observers_copy; { std::lock_guardstd::mutex lock(mtx_); observers_copy observers_; // 复制列表减少锁持有时间 } for (Observer* observer : observers_copy) { observer-update(temperature_, humidity_, pressure_); } } // ... removeObserver也需要加锁 };注意在notifyObservers中我们先复制观察者列表然后在没有锁保护的情况下进行通知。这是为了避免在调用观察者的update方法时该方法可能执行很长时间或尝试获取其他锁仍然持有mtx_从而导致死锁或降低并发性能。但复制操作有开销且复制后原列表的变更如新注册的观察者不会在这次通知中生效这需要根据业务逻辑判断是否可接受。并发容器使用如Intel TBB或folly库提供的并发向量但C标准库目前没有现成的线程安全容器直接适用于此场景。无锁编程或RCU对于高性能场景可以考虑更复杂的无锁数据结构或Read-Copy-Update机制但这大大增加了实现复杂度。核心建议对于大多数应用使用互斥锁并小心管理锁的粒度如复制列表后通知是平衡安全性和复杂度的合理选择。务必在文档中明确说明该主题类的线程安全属性。5.2 内存管理与生命周期这是我们示例中留下的一个“坑”使用原始指针管理观察者生命周期风险很高。问题如果观察者对象在尚未从主题中注销的情况下就被销毁例如局部对象超出作用域或者被手动delete主题持有的指针就变成了“悬垂指针”。下次通知时通过这个指针调用update就是未定义行为通常导致程序崩溃。解决方案使用std::shared_ptr和std::weak_ptr推荐观察者对象由std::shared_ptrObserver管理。主题内部存储std::weak_ptrObserver。弱指针不会增加引用计数因此不会阻止观察者被销毁。在notifyObservers时遍历弱指针列表尝试将每个weak_ptr提升lock()为shared_ptr。如果提升成功说明观察者还活着可以安全调用update如果失败返回空说明观察者已销毁则可以从列表中移除这个失效的弱指针。#include memory #include vector class Subject { public: virtual void registerObserver(std::weak_ptrObserver o) 0; // removeObserver可能不再需要因为可以通过检查weak_ptr是否过期来清理。 virtual void notifyObservers() 0; }; class WeatherData : public Subject { private: std::vectorstd::weak_ptrObserver observers_; // ... void notifyObservers() override { auto it observers_.begin(); while (it ! observers_.end()) { if (auto observer_sp it-lock()) { // 观察者还活着安全通知 observer_sp-update(temperature_, humidity_, pressure_); it; } else { // 观察者已销毁从列表中移除失效的弱指针 it observers_.erase(it); } } } };这种方式自动处理了生命周期问题但引入了智能指针的开销和稍微复杂一些的语法。使用观察者令牌Token让registerObserver返回一个唯一的令牌如整数ID或std::string观察者或管理观察者的代码需要保存这个令牌。当观察者要注销时或在其析构函数中必须使用这个令牌调用主题的removeObserver。这要求观察者或其所有者有明确的注销意识。主题拥有观察者的所有权在某些设计下主题负责创建和销毁所有观察者。这适用于观察者生命周期完全由主题控制的场景耦合度较高。实操心得在新项目中如果条件允许我强烈建议采用weak_ptr方案。它虽然看起来复杂一点但一劳永逸地解决了生命周期管理的核心痛点让代码更健壮。对于老项目或性能极其敏感的模块如果使用原始指针则必须建立严格的编程规范确保观察者在销毁前一定注销并考虑使用辅助类如RAII包装器来自动化注册/注销过程。5.3 通知的顺序与性能通知顺序std::vector的遍历顺序是注册顺序。有时业务可能要求特定的通知顺序如优先级。你可以改用std::list并自定义插入逻辑或者在观察者接口中增加一个优先级字段主题使用优先队列如std::multiset来存储。性能考量notifyObservers是O(N)操作N是观察者数量。如果N非常大成千上万且update操作很轻量通知过程可能成为瓶颈。可以考虑批量更新、异步通知或将观察者分组。在update方法内观察者应避免执行耗时操作或调用可能阻塞的函数否则会拖慢整个通知流程影响其他观察者和主题本身的响应性。对于耗时任务应考虑在观察者内部将其抛到另一个线程或线程池中执行。6. 常见问题排查与设计模式变体6.1 实战中踩过的坑通知循环或过频通知在观察者的update方法中如果又调用了主题的某个方法而该方法会再次触发setMeasurements和notifyObservers就可能形成无限递归或意料之外的多重通知。务必检查update方法中的逻辑确保不会间接导致主题状态被再次修改。有时需要在主题中设置一个“更新中”的标志位来防止重入。观察者update方法抛出异常如果某个观察者的update方法抛出异常会导致整个通知循环中断后面的观察者收不到通知。这可能是灾难性的。一种做法是在notifyObservers内部对每个观察者的调用进行try-catch记录错误但继续通知其他观察者。但这需要谨慎设计异常处理策略。忘记在析构函数中取消注册这是使用原始指针时最常见的错误。务必在观察者析构函数中调用subject_-removeObserver(this)。使用RAII思想将注册操作放在构造函数注销操作放在析构函数可以很大程度上避免这个问题。“拉”模型中的数据竞争如果使用“拉”模型update(Subject)观察者在update方法内部调用subject.getTemperature()等方法。在多线程环境下如果主题的数据在notifyObservers调用之后、观察者拉取数据之前被另一个线程修改观察者可能读到不一致的快照。这需要结合主题数据的线程安全设计来考虑。6.2 观察者模式的变体与相关模式发布-订阅模式观察者模式常与发布-订阅模式混淆。两者核心思想相似但发布-订阅模式通常引入了一个“事件通道”或“消息代理”作为中介发布者和订阅者完全不知道对方的存在耦合度更低。我们的简单气象站是标准的观察者模式。如果气象站将数据发送到一个消息队列如RabbitMQ、Kafka各个显示屏从队列订阅数据那就是发布-订阅。事件监听器在GUI编程如Qt、MFC或JavaScript中非常常见本质上是观察者模式的应用。按钮主题拥有点击事件多个处理函数观察者可以监听这个事件。反应式编程像RxCpp这类库将观察者模式的思想推向极致提供了强大的数据流组合、变换和异步处理能力。你可以把气象数据看作一个数据流Observable各个显示屏是订阅这个流的观察者Subscriber。6.3 何时使用与何时避免使用场景当一个对象的改变需要同时改变其他对象并且你不知道具体有多少对象有待改变时。当一个对象必须通知其他对象但又希望避免这些对象之间紧密耦合时。跨层或跨模块通信例如业务逻辑层需要通知多个UI组件更新。避免场景如果观察者数量极少且固定直接调用可能更简单。如果通知链过于复杂可能导致难以调试的级联更新。对于性能要求极其苛刻的实时系统遍历观察者列表的开销可能需要评估。最后观察者模式是工具箱里一件非常趁手的兵器。理解其原理和实现中的各种细节尤其是生命周期和线程安全能让你在面临对象间复杂的动态依赖关系时写出更清晰、更灵活、更易于维护的C代码。这个气象站的小例子几乎包含了模式最核心的骨架你可以在此基础上根据实际项目需求填充血肉应对更复杂的场景。