设计模式入门:6. 观察者模式详解 C++实现
2026/6/3 23:23:30 网站建设 项目流程

观察者模式详解:实现对象间的"一对多"通知,C++完整实现

引言

你有没有订阅过公众号?当你关注了一个公众号后,只要它发布了新文章,你就会自动收到推送通知。你不需要每天主动去查看公众号有没有更新,公众号会在有新内容时主动告诉你。

在软件开发中,我们也经常会遇到类似的场景:一个对象的状态发生了变化,需要通知其他多个对象做出相应的反应。比如:

  • 天气预报更新后,所有显示天气的界面都要刷新
  • 股票价格变动后,所有股票行情显示和交易系统都要更新
  • 按钮被点击后,所有注册了点击事件的处理函数都要执行
  • 配置文件修改后,所有使用该配置的模块都要重新加载

如果用传统的方式实现,我们需要让每个对象都持有其他所有需要通知的对象的引用,这会导致代码耦合度极高,难以维护和扩展。

观察者模式(Observer Pattern)正是为了解决这个问题而生的。它是一种行为型设计模式,定义了对象之间的一对多依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都会自动收到通知并更新。

今天我们就用C++语言,从基础概念到完整实现,全面深入地理解观察者模式。


一、观察者模式的核心概念

1.1 解决的痛点

在没有观察者模式的情况下,实现对象间的通知通常有两种方式:

  1. 轮询方式:观察者定期主动查询主题的状态。这种方式效率极低,会浪费大量CPU资源,而且实时性差。
  2. 硬编码方式:主题对象持有所有观察者的引用,状态改变时逐个调用它们的更新方法。这种方式耦合度极高,新增或删除观察者都需要修改主题代码,违反开闭原则。

观察者模式采用**“发布-订阅(Publish-Subscribe)”**的思想,完美解决了这些问题。主题对象不需要知道任何观察者的存在,它只负责在状态改变时发布通知;观察者对象只需要订阅自己感兴趣的主题,当主题有更新时会自动收到通知。

1.2 核心思想

观察者模式的核心思想是:将对象分为"主题"(被观察者)和"观察者"两类。主题负责管理所有订阅了它的观察者,并在自身状态发生改变时,自动通知所有观察者

这种方式实现了主题和观察者之间的松耦合

  • 主题不知道观察者的具体实现,只知道它们都实现了同一个观察者接口
  • 观察者不知道主题的具体实现,只知道主题提供了订阅和取消订阅的接口
  • 主题和观察者可以独立变化,互不影响

1.3 四个核心角色

观察者模式包含四个关键角色:

  1. 抽象主题(Subject):定义了主题的通用接口,声明了注册、移除和通知观察者的方法
  2. 具体主题(Concrete Subject):被观察的对象,维护自身的状态,当状态改变时通知所有注册的观察者
  3. 抽象观察者(Observer):定义了观察者的通用接口,声明了接收通知和更新的方法
  4. 具体观察者(Concrete Observer):实现了抽象观察者接口,在收到主题通知时更新自身的状态或执行相应的操作

二、标准观察者模式实现

2.1 UML类图

2.2 C++实现(天气站例子)

我们用最经典的"天气站"例子来实现观察者模式。假设我们有一个天气数据采集站,它会实时采集温度、湿度和气压数据。当这些数据更新时,我们需要通知多个显示设备(当前天气显示、统计显示、预报显示)更新它们的显示内容。

#include<iostream>#include<string>#include<vector>#include<memory>#include<algorithm>// 前向声明classObserver;// 抽象主题:天气主题classWeatherSubject{public:virtual~WeatherSubject()=default;// 注册观察者virtualvoidattach(std::shared_ptr<Observer>observer)=0;// 移除观察者virtualvoiddetach(std::shared_ptr<Observer>observer)=0;// 通知所有观察者virtualvoidnotifyObservers()=0;};// 抽象观察者classObserver{public:virtual~Observer()=default;// 更新方法,由主题调用virtualvoidupdate(floattemperature,floathumidity,floatpressure)=0;};// 具体主题:天气数据classWeatherData:publicWeatherSubject{private:std::vector<std::shared_ptr<Observer>>observers_;// 观察者列表floattemperature_;// 温度floathumidity_;// 湿度floatpressure_;// 气压public:// 注册观察者voidattach(std::shared_ptr<Observer>observer)override{observers_.push_back(observer);}// 移除观察者voiddetach(std::shared_ptr<Observer>observer)override{autoit=std::find(observers_.begin(),observers_.end(),observer);if(it!=observers_.end()){observers_.erase(it);}}// 通知所有观察者voidnotifyObservers()override{// 复制一份观察者列表,避免在通知过程中列表被修改导致迭代器失效autoobservers_copy=observers_;for(constauto&observer:observers_copy){observer->update(temperature_,humidity_,pressure_);}}// 当天气数据更新时调用voidmeasurementsChanged(){notifyObservers();}// 设置天气数据voidsetMeasurements(floattemperature,floathumidity,floatpressure){temperature_=temperature;humidity_=humidity;pressure_=pressure;measurementsChanged();}// 获取天气数据(拉模式时使用)floatgetTemperature()const{returntemperature_;}floatgetHumidity()const{returnhumidity_;}floatgetPressure()const{returnpressure_;}};// 具体观察者1:当前天气显示classCurrentConditionsDisplay:publicObserver{private:floattemperature_;floathumidity_;std::shared_ptr<WeatherData>weather_data_;public:explicitCurrentConditionsDisplay(std::shared_ptr<WeatherData>weather_data):weather_data_(weather_data){// 注册自己到主题weather_data_->attach(shared_from_this());}// 更新显示voidupdate(floattemperature,floathumidity,floatpressure)override{temperature_=temperature;humidity_=humidity;display();}// 显示当前天气voiddisplay()const{std::cout<<"当前天气: 温度 "<<temperature_<<"°C, 湿度 "<<humidity_<<"%"<<std::endl;}};// 具体观察者2:天气统计显示classStatisticsDisplay:publicObserver{private:floatmax_temp_=-100.0f;floatmin_temp_=100.0f;floatsum_temp_=0.0f;intcount_=0;std::shared_ptr<WeatherData>weather_data_;public:explicitStatisticsDisplay(std::shared_ptr<WeatherData>weather_data):weather_data_(weather_data){weather_data_->attach(shared_from_this());}voidupdate(floattemperature,floathumidity,floatpressure)override{sum_temp_+=temperature;count_++;if(temperature>max_temp_)max_temp_=temperature;if(temperature<min_temp_)min_temp_=temperature;display();}voiddisplay()const{std::cout<<"天气统计: 平均温度 "<<sum_temp_/count_<<"°C, 最高温度 "<<max_temp_<<"°C, 最低温度 "<<min_temp_<<"°C"<<std::endl;}};// 具体观察者3:天气预报显示classForecastDisplay:publicObserver{private:floatcurrent_pressure_=1013.0f;floatlast_pressure_;std::shared_ptr<WeatherData>weather_data_;public:explicitForecastDisplay(std::shared_ptr<WeatherData>weather_data):weather_data_(weather_data){weather_data_->attach(shared_from_this());}voidupdate(floattemperature,floathumidity,floatpressure)override{last_pressure_=current_pressure_;current_pressure_=pressure;display();}voiddisplay()const{std::cout<<"天气预报: ";if(current_pressure_>last_pressure_){std::cout<<"气压上升,天气将转晴"<<std::endl;}elseif(current_pressure_<last_pressure_){std::cout<<"气压下降,可能会下雨"<<std::endl;}else{std::cout<<"气压稳定,天气保持不变"<<std::endl;}}};// 客户端代码intmain(){// 创建天气数据主题autoweather_data=std::make_shared<WeatherData>();// 创建观察者并注册到主题autocurrent_display=std::make_shared<CurrentConditionsDisplay>(weather_data);autostatistics_display=std::make_shared<StatisticsDisplay>(weather_data);autoforecast_display=std::make_shared<ForecastDisplay>(weather_data);std::cout<<"=== 第一次天气更新 ==="<<std::endl;weather_data->setMeasurements(25.5f,65.0f,1012.0f);std::cout<<"\n=== 第二次天气更新 ==="<<std::endl;weather_data->setMeasurements(28.0f,70.0f,1010.0f);std::cout<<"\n=== 移除天气预报显示 ==="<<std::endl;weather_data->detach(forecast_display);std::cout<<"\n=== 第三次天气更新 ==="<<std::endl;weather_data->setMeasurements(26.0f,60.0f,1015.0f);return0;}

2.3 运行结果

=== 第一次天气更新 === 当前天气: 温度 25.5°C, 湿度 65% 天气统计: 平均温度 25.5°C, 最高温度 25.5°C, 最低温度 25.5°C 天气预报: 气压下降,可能会下雨 === 第二次天气更新 === 当前天气: 温度 28°C, 湿度 70% 天气统计: 平均温度 26.75°C, 最高温度 28°C, 最低温度 25.5°C 天气预报: 气压下降,可能会下雨 === 移除天气预报显示 === === 第三次天气更新 === 当前天气: 温度 26°C, 湿度 60% 天气统计: 平均温度 26.5°C, 最高温度 28°C, 最低温度 25.5°C

2.4 代码解析

  • 抽象主题WeatherSubject:定义了attachdetachnotifyObservers三个核心方法,所有具体主题都必须实现这些方法
  • 具体主题WeatherData:维护天气数据(温度、湿度、气压),并管理观察者列表。当天气数据更新时,调用notifyObservers方法通知所有观察者
  • 抽象观察者Observer:定义了update方法,所有具体观察者都必须实现这个方法来接收通知
  • 具体观察者CurrentConditionsDisplayStatisticsDisplayForecastDisplay,它们在构造时自动注册到主题,在收到通知时更新自己的显示内容

关键细节

  • notifyObservers方法中,我们先复制了一份观察者列表再进行遍历。这是为了避免在通知过程中,有观察者被移除或添加,导致迭代器失效
  • 使用std::shared_ptr来管理对象的生命周期,避免内存泄漏
  • 观察者在构造时自动注册到主题,简化了客户端代码

三、推模式 vs 拉模式

观察者模式有两种不同的通知方式:推模式拉模式,它们各有优缺点,适用于不同的场景。

3.1 推模式

推模式是指主题在通知观察者时,将所有相关的数据都推送给观察者。上面我们实现的就是推模式,update方法接收了温度、湿度、气压三个参数。

优点

  • 观察者不需要主动查询数据,使用方便
  • 数据传输效率高,一次通知传递所有数据

缺点

  • 不够灵活,如果主题新增了数据字段,所有观察者的update方法都需要修改
  • 可能会传递观察者不需要的数据,造成浪费

3.2 拉模式

拉模式是指主题在通知观察者时,只告诉观察者"我的状态改变了",不传递任何数据。观察者收到通知后,主动从主题拉取自己需要的数据。

拉模式实现示意

// 抽象观察者(拉模式)classObserver{public:virtual~Observer()=default;// 拉模式下update方法不接收参数virtualvoidupdate()=0;};// 具体观察者(拉模式)classCurrentConditionsDisplay:publicObserver{private:std::shared_ptr<WeatherData>weather_data_;public:explicitCurrentConditionsDisplay(std::shared_ptr<WeatherData>weather_data):weather_data_(weather_data){weather_data_->attach(shared_from_this());}voidupdate()override{// 主动从主题拉取需要的数据floattemp=weather_data_->getTemperature();floathumidity=weather_data_->getHumidity();std::cout<<"当前天气: 温度 "<<temp<<"°C, 湿度 "<<humidity<<"%"<<std::endl;}};// 具体主题的notifyObservers方法(拉模式)voidnotifyObservers()override{autoobservers_copy=observers_;for(constauto&observer:observers_copy){observer->update();// 不传递任何参数}}

优点

  • 非常灵活,主题新增数据字段时,不需要修改观察者的update方法
  • 观察者只拉取自己需要的数据,不会浪费资源

缺点

  • 观察者需要知道主题的接口,增加了耦合度
  • 多个观察者可能会重复拉取相同的数据,降低效率

3.3 如何选择

  • 如果观察者需要的数据比较固定,且所有观察者需要的数据都差不多,使用推模式
  • 如果观察者需要的数据各不相同,且未来可能会新增数据字段,使用拉模式
  • 在实际开发中,也可以结合两种模式的优点,主题推送一个包含所有数据的对象,观察者从中提取自己需要的数据

四、观察者模式的优缺点

4.1 优点

  1. 松耦合:主题和观察者之间是抽象耦合,它们只知道对方实现了对应的接口,不需要知道具体实现
  2. 支持广播通信:主题一次通知可以发送给所有注册的观察者,非常高效
  3. 符合开闭原则:新增观察者不需要修改主题代码,新增主题也不需要修改现有观察者代码
  4. 可以动态添加和移除观察者:在运行时可以随时改变观察者的数量和类型
  5. 职责单一:主题只负责管理观察者和发布通知,观察者只负责处理自己的更新逻辑

4.2 缺点

  1. 通知顺序不确定:观察者收到通知的顺序是不确定的,不能依赖通知顺序来编写逻辑
  2. 性能开销:如果观察者数量很多,通知所有观察者会有一定的性能开销
  3. 可能导致循环依赖:如果观察者同时也是主题,可能会导致循环通知,最终导致栈溢出
  4. 主题不知道更新结果:主题只负责发送通知,不知道观察者是否成功处理了通知
  5. 可能产生内存泄漏:如果观察者没有正确取消订阅,主题会一直持有观察者的引用,导致观察者无法被释放

五、适用场景

观察者模式特别适合以下场景:

  1. 事件驱动系统:如GUI系统中的按钮点击、键盘输入等事件处理
  2. 消息通知系统:如公众号推送、邮件通知、短信通知等
  3. 数据同步系统:如数据库数据变更后,同步到缓存、搜索引擎等
  4. 实时监控系统:如股票行情监控、服务器性能监控等
  5. MVC架构:Model和View之间的通信就是典型的观察者模式,Model是主题,View是观察者

经典应用案例

  • Java的java.util.Observerjava.util.Observable(虽然已被废弃,但思想是一样的)
  • C#的事件和委托机制
  • JavaScript的事件监听机制
  • Qt的信号与槽机制
  • 各种消息队列和发布-订阅系统

六、现代C++改进与变种

6.1 使用std::function和Lambda简化观察者

在C++11及以后,我们可以使用std::function和Lambda表达式来简化观察者模式的实现,不需要定义抽象观察者类:

#include<functional>#include<vector>// 现代C++风格的主题类classWeatherStation{private:// 使用std::function作为观察者类型usingObserver=std::function<void(float,float,float)>;std::vector<Observer>observers_;floattemperature_;floathumidity_;floatpressure_;public:// 注册观察者voidsubscribe(Observer observer){observers_.push_back(std::move(observer));}// 通知所有观察者voidnotify(){for(constauto&observer:observers_){observer(temperature_,humidity_,pressure_);}}// 设置天气数据voidsetMeasurements(floattemp,floathumidity,floatpressure){temperature_=temp;humidity_=humidity;pressure_=pressure;notify();}};// 客户端代码intmain(){WeatherStation station;// 使用Lambda表达式作为观察者station.subscribe([](floattemp,floathumidity,floatpressure){std::cout<<"Lambda观察者: 温度 "<<temp<<"°C"<<std::endl;});station.subscribe([](floattemp,floathumidity,floatpressure){std::cout<<"Lambda观察者: 湿度 "<<humidity<<"%"<<std::endl;});station.setMeasurements(25.0f,60.0f,1013.0f);return0;}

这种方式非常简洁灵活,不需要定义任何观察者类,直接使用Lambda表达式作为观察者。

6.2 通用事件总线

我们可以基于观察者模式实现一个通用的事件总线,支持不同类型的事件:

#include<any>#include<unordered_map>#include<typeindex>classEventBus{private:usingHandler=std::function<void(conststd::any&)>;std::unordered_map<std::type_index,std::vector<Handler>>handlers_;public:// 订阅事件template<typenameEventType>voidsubscribe(std::function<void(constEventType&)>handler){handlers_[std::type_index(typeid(EventType))].push_back([handler=std::move(handler)](conststd::any&event){handler(std::any_cast<constEventType&>(event));});}// 发布事件template<typenameEventType>voidpublish(constEventType&event){autoit=handlers_.find(std::type_index(typeid(EventType)));if(it!=handlers_.end()){for(constauto&handler:it->second){handler(event);}}}};// 定义事件类型structTemperatureChangedEvent{floatnew_temperature;};structHumidityChangedEvent{floatnew_humidity;};// 客户端代码intmain(){EventBus bus;bus.subscribe<TemperatureChangedEvent>([](constTemperatureChangedEvent&e){std::cout<<"温度更新: "<<e.new_temperature<<"°C"<<std::endl;});bus.subscribe<HumidityChangedEvent>([](constHumidityChangedEvent&e){std::cout<<"湿度更新: "<<e.new_humidity<<"%"<<std::endl;});bus.publish(TemperatureChangedEvent{25.5f});bus.publish(HumidityChangedEvent{65.0f});return0;}

这种通用事件总线在实际项目中非常常用,可以大大降低模块之间的耦合度。


七、实际应用中的注意事项

  1. 避免循环依赖:如果观察者同时也是主题,一定要注意避免循环通知,否则会导致栈溢出
  2. 正确取消订阅:观察者在销毁前一定要取消订阅,否则主题会一直持有观察者的引用,导致内存泄漏
  3. 处理异常:如果某个观察者在处理通知时抛出异常,可能会影响其他观察者的处理。可以在通知时捕获异常,保证所有观察者都能收到通知
  4. 考虑异步通知:如果观察者的处理逻辑比较耗时,可以考虑使用异步通知,避免阻塞主题线程
  5. 注意线程安全:如果在多线程环境中使用观察者模式,需要对观察者列表的访问进行同步

八、总结

观察者模式是一种非常重要且常用的设计模式,它的核心思想是“发布-订阅”,实现了对象之间一对多的依赖关系,让主题和观察者之间松耦合。

在实际开发中,观察者模式无处不在。从GUI系统的事件处理,到消息队列的发布订阅,再到MVC架构的Model-View通信,都能看到观察者模式的身影。

现代C++的std::function和Lambda表达式让观察者模式的实现变得更加简洁和灵活。基于观察者模式实现的事件总线,已经成为大型项目中解耦模块的标准方式。

记住,设计模式不是银弹。只有当你确实需要实现对象间的一对多通知,并且希望主题和观察者之间松耦合时,才应该使用观察者模式。

希望这篇文章能帮助你彻底理解观察者模式,并在实际项目中正确地使用它。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询