设计模式入门:6. 观察者模式详解 C++实现
观察者模式详解实现对象间的一对多通知C完整实现引言你有没有订阅过公众号当你关注了一个公众号后只要它发布了新文章你就会自动收到推送通知。你不需要每天主动去查看公众号有没有更新公众号会在有新内容时主动告诉你。在软件开发中我们也经常会遇到类似的场景一个对象的状态发生了变化需要通知其他多个对象做出相应的反应。比如天气预报更新后所有显示天气的界面都要刷新股票价格变动后所有股票行情显示和交易系统都要更新按钮被点击后所有注册了点击事件的处理函数都要执行配置文件修改后所有使用该配置的模块都要重新加载如果用传统的方式实现我们需要让每个对象都持有其他所有需要通知的对象的引用这会导致代码耦合度极高难以维护和扩展。观察者模式(Observer Pattern)正是为了解决这个问题而生的。它是一种行为型设计模式定义了对象之间的一对多依赖关系当一个对象的状态发生改变时所有依赖它的对象都会自动收到通知并更新。今天我们就用C语言从基础概念到完整实现全面深入地理解观察者模式。一、观察者模式的核心概念1.1 解决的痛点在没有观察者模式的情况下实现对象间的通知通常有两种方式轮询方式观察者定期主动查询主题的状态。这种方式效率极低会浪费大量CPU资源而且实时性差。硬编码方式主题对象持有所有观察者的引用状态改变时逐个调用它们的更新方法。这种方式耦合度极高新增或删除观察者都需要修改主题代码违反开闭原则。观察者模式采用**“发布-订阅(Publish-Subscribe)”**的思想完美解决了这些问题。主题对象不需要知道任何观察者的存在它只负责在状态改变时发布通知观察者对象只需要订阅自己感兴趣的主题当主题有更新时会自动收到通知。1.2 核心思想观察者模式的核心思想是将对象分为主题(被观察者)和观察者两类。主题负责管理所有订阅了它的观察者并在自身状态发生改变时自动通知所有观察者。这种方式实现了主题和观察者之间的松耦合主题不知道观察者的具体实现只知道它们都实现了同一个观察者接口观察者不知道主题的具体实现只知道主题提供了订阅和取消订阅的接口主题和观察者可以独立变化互不影响1.3 四个核心角色观察者模式包含四个关键角色抽象主题(Subject)定义了主题的通用接口声明了注册、移除和通知观察者的方法具体主题(Concrete Subject)被观察的对象维护自身的状态当状态改变时通知所有注册的观察者抽象观察者(Observer)定义了观察者的通用接口声明了接收通知和更新的方法具体观察者(Concrete Observer)实现了抽象观察者接口在收到主题通知时更新自身的状态或执行相应的操作二、标准观察者模式实现2.1 UML类图2.2 C实现天气站例子我们用最经典的天气站例子来实现观察者模式。假设我们有一个天气数据采集站它会实时采集温度、湿度和气压数据。当这些数据更新时我们需要通知多个显示设备当前天气显示、统计显示、预报显示更新它们的显示内容。#includeiostream#includestring#includevector#includememory#includealgorithm// 前向声明classObserver;// 抽象主题天气主题classWeatherSubject{public:virtual~WeatherSubject()default;// 注册观察者virtualvoidattach(std::shared_ptrObserverobserver)0;// 移除观察者virtualvoiddetach(std::shared_ptrObserverobserver)0;// 通知所有观察者virtualvoidnotifyObservers()0;};// 抽象观察者classObserver{public:virtual~Observer()default;// 更新方法由主题调用virtualvoidupdate(floattemperature,floathumidity,floatpressure)0;};// 具体主题天气数据classWeatherData:publicWeatherSubject{private:std::vectorstd::shared_ptrObserverobservers_;// 观察者列表floattemperature_;// 温度floathumidity_;// 湿度floatpressure_;// 气压public:// 注册观察者voidattach(std::shared_ptrObserverobserver)override{observers_.push_back(observer);}// 移除观察者voiddetach(std::shared_ptrObserverobserver)override{autoitstd::find(observers_.begin(),observers_.end(),observer);if(it!observers_.end()){observers_.erase(it);}}// 通知所有观察者voidnotifyObservers()override{// 复制一份观察者列表避免在通知过程中列表被修改导致迭代器失效autoobservers_copyobservers_;for(constautoobserver: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_ptrWeatherDataweather_data_;public:explicitCurrentConditionsDisplay(std::shared_ptrWeatherDataweather_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_ptrWeatherDataweather_data_;public:explicitStatisticsDisplay(std::shared_ptrWeatherDataweather_data):weather_data_(weather_data){weather_data_-attach(shared_from_this());}voidupdate(floattemperature,floathumidity,floatpressure)override{sum_temp_temperature;count_;if(temperaturemax_temp_)max_temp_temperature;if(temperaturemin_temp_)min_temp_temperature;display();}voiddisplay()const{std::cout天气统计: 平均温度 sum_temp_/count_°C, 最高温度 max_temp_°C, 最低温度 min_temp_°Cstd::endl;}};// 具体观察者3天气预报显示classForecastDisplay:publicObserver{private:floatcurrent_pressure_1013.0f;floatlast_pressure_;std::shared_ptrWeatherDataweather_data_;public:explicitForecastDisplay(std::shared_ptrWeatherDataweather_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_datastd::make_sharedWeatherData();// 创建观察者并注册到主题autocurrent_displaystd::make_sharedCurrentConditionsDisplay(weather_data);autostatistics_displaystd::make_sharedStatisticsDisplay(weather_data);autoforecast_displaystd::make_sharedForecastDisplay(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°C2.4 代码解析抽象主题WeatherSubject定义了attach、detach和notifyObservers三个核心方法所有具体主题都必须实现这些方法具体主题WeatherData维护天气数据温度、湿度、气压并管理观察者列表。当天气数据更新时调用notifyObservers方法通知所有观察者抽象观察者Observer定义了update方法所有具体观察者都必须实现这个方法来接收通知具体观察者CurrentConditionsDisplay、StatisticsDisplay、ForecastDisplay它们在构造时自动注册到主题在收到通知时更新自己的显示内容关键细节在notifyObservers方法中我们先复制了一份观察者列表再进行遍历。这是为了避免在通知过程中有观察者被移除或添加导致迭代器失效使用std::shared_ptr来管理对象的生命周期避免内存泄漏观察者在构造时自动注册到主题简化了客户端代码三、推模式 vs 拉模式观察者模式有两种不同的通知方式推模式和拉模式它们各有优缺点适用于不同的场景。3.1 推模式推模式是指主题在通知观察者时将所有相关的数据都推送给观察者。上面我们实现的就是推模式update方法接收了温度、湿度、气压三个参数。优点观察者不需要主动查询数据使用方便数据传输效率高一次通知传递所有数据缺点不够灵活如果主题新增了数据字段所有观察者的update方法都需要修改可能会传递观察者不需要的数据造成浪费3.2 拉模式拉模式是指主题在通知观察者时只告诉观察者我的状态改变了不传递任何数据。观察者收到通知后主动从主题拉取自己需要的数据。拉模式实现示意// 抽象观察者拉模式classObserver{public:virtual~Observer()default;// 拉模式下update方法不接收参数virtualvoidupdate()0;};// 具体观察者拉模式classCurrentConditionsDisplay:publicObserver{private:std::shared_ptrWeatherDataweather_data_;public:explicitCurrentConditionsDisplay(std::shared_ptrWeatherDataweather_data):weather_data_(weather_data){weather_data_-attach(shared_from_this());}voidupdate()override{// 主动从主题拉取需要的数据floattempweather_data_-getTemperature();floathumidityweather_data_-getHumidity();std::cout当前天气: 温度 temp°C, 湿度 humidity%std::endl;}};// 具体主题的notifyObservers方法拉模式voidnotifyObservers()override{autoobservers_copyobservers_;for(constautoobserver:observers_copy){observer-update();// 不传递任何参数}}优点非常灵活主题新增数据字段时不需要修改观察者的update方法观察者只拉取自己需要的数据不会浪费资源缺点观察者需要知道主题的接口增加了耦合度多个观察者可能会重复拉取相同的数据降低效率3.3 如何选择如果观察者需要的数据比较固定且所有观察者需要的数据都差不多使用推模式如果观察者需要的数据各不相同且未来可能会新增数据字段使用拉模式在实际开发中也可以结合两种模式的优点主题推送一个包含所有数据的对象观察者从中提取自己需要的数据四、观察者模式的优缺点4.1 优点松耦合主题和观察者之间是抽象耦合它们只知道对方实现了对应的接口不需要知道具体实现支持广播通信主题一次通知可以发送给所有注册的观察者非常高效符合开闭原则新增观察者不需要修改主题代码新增主题也不需要修改现有观察者代码可以动态添加和移除观察者在运行时可以随时改变观察者的数量和类型职责单一主题只负责管理观察者和发布通知观察者只负责处理自己的更新逻辑4.2 缺点通知顺序不确定观察者收到通知的顺序是不确定的不能依赖通知顺序来编写逻辑性能开销如果观察者数量很多通知所有观察者会有一定的性能开销可能导致循环依赖如果观察者同时也是主题可能会导致循环通知最终导致栈溢出主题不知道更新结果主题只负责发送通知不知道观察者是否成功处理了通知可能产生内存泄漏如果观察者没有正确取消订阅主题会一直持有观察者的引用导致观察者无法被释放五、适用场景观察者模式特别适合以下场景事件驱动系统如GUI系统中的按钮点击、键盘输入等事件处理消息通知系统如公众号推送、邮件通知、短信通知等数据同步系统如数据库数据变更后同步到缓存、搜索引擎等实时监控系统如股票行情监控、服务器性能监控等MVC架构Model和View之间的通信就是典型的观察者模式Model是主题View是观察者经典应用案例Java的java.util.Observer和java.util.Observable虽然已被废弃但思想是一样的C#的事件和委托机制JavaScript的事件监听机制Qt的信号与槽机制各种消息队列和发布-订阅系统六、现代C改进与变种6.1 使用std::function和Lambda简化观察者在C11及以后我们可以使用std::function和Lambda表达式来简化观察者模式的实现不需要定义抽象观察者类#includefunctional#includevector// 现代C风格的主题类classWeatherStation{private:// 使用std::function作为观察者类型usingObserverstd::functionvoid(float,float,float);std::vectorObserverobservers_;floattemperature_;floathumidity_;floatpressure_;public:// 注册观察者voidsubscribe(Observer observer){observers_.push_back(std::move(observer));}// 通知所有观察者voidnotify(){for(constautoobserver: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::coutLambda观察者: 温度 temp°Cstd::endl;});station.subscribe([](floattemp,floathumidity,floatpressure){std::coutLambda观察者: 湿度 humidity%std::endl;});station.setMeasurements(25.0f,60.0f,1013.0f);return0;}这种方式非常简洁灵活不需要定义任何观察者类直接使用Lambda表达式作为观察者。6.2 通用事件总线我们可以基于观察者模式实现一个通用的事件总线支持不同类型的事件#includeany#includeunordered_map#includetypeindexclassEventBus{private:usingHandlerstd::functionvoid(conststd::any);std::unordered_mapstd::type_index,std::vectorHandlerhandlers_;public:// 订阅事件templatetypenameEventTypevoidsubscribe(std::functionvoid(constEventType)handler){handlers_[std::type_index(typeid(EventType))].push_back([handlerstd::move(handler)](conststd::anyevent){handler(std::any_castconstEventType(event));});}// 发布事件templatetypenameEventTypevoidpublish(constEventTypeevent){autoithandlers_.find(std::type_index(typeid(EventType)));if(it!handlers_.end()){for(constautohandler:it-second){handler(event);}}}};// 定义事件类型structTemperatureChangedEvent{floatnew_temperature;};structHumidityChangedEvent{floatnew_humidity;};// 客户端代码intmain(){EventBus bus;bus.subscribeTemperatureChangedEvent([](constTemperatureChangedEvente){std::cout温度更新: e.new_temperature°Cstd::endl;});bus.subscribeHumidityChangedEvent([](constHumidityChangedEvente){std::cout湿度更新: e.new_humidity%std::endl;});bus.publish(TemperatureChangedEvent{25.5f});bus.publish(HumidityChangedEvent{65.0f});return0;}这种通用事件总线在实际项目中非常常用可以大大降低模块之间的耦合度。七、实际应用中的注意事项避免循环依赖如果观察者同时也是主题一定要注意避免循环通知否则会导致栈溢出正确取消订阅观察者在销毁前一定要取消订阅否则主题会一直持有观察者的引用导致内存泄漏处理异常如果某个观察者在处理通知时抛出异常可能会影响其他观察者的处理。可以在通知时捕获异常保证所有观察者都能收到通知考虑异步通知如果观察者的处理逻辑比较耗时可以考虑使用异步通知避免阻塞主题线程注意线程安全如果在多线程环境中使用观察者模式需要对观察者列表的访问进行同步八、总结观察者模式是一种非常重要且常用的设计模式它的核心思想是“发布-订阅”实现了对象之间一对多的依赖关系让主题和观察者之间松耦合。在实际开发中观察者模式无处不在。从GUI系统的事件处理到消息队列的发布订阅再到MVC架构的Model-View通信都能看到观察者模式的身影。现代C的std::function和Lambda表达式让观察者模式的实现变得更加简洁和灵活。基于观察者模式实现的事件总线已经成为大型项目中解耦模块的标准方式。记住设计模式不是银弹。只有当你确实需要实现对象间的一对多通知并且希望主题和观察者之间松耦合时才应该使用观察者模式。希望这篇文章能帮助你彻底理解观察者模式并在实际项目中正确地使用它。