设计模式---观察者模式
观察者模式(Observer Pattern)是行为型设计模式的核心成员,其核心思想是定义对象间的一对多依赖关系——当“被观察者”(主题)的状态发生变化时,所有依赖它的“观察者”对象会自动收到通知并执行更新操作。这种模式的本质是解耦主题与观察者,让两者可以独立演化,同时保证状态变化的“广播式”传递。在C++程序设计中,观察者模式广泛应用于GUI事件处理、监控系统、日志订阅、消息通知等场景,是构建松耦合系统的关键工具。
一、观察者模式的核心定义与设计意图
在软件设计中,经常会遇到“一个对象变化导致多个对象需要同步更新”的场景(例如:气象站数据变化时,手机APP、显示屏、报警器需同时更新)。若直接在主题中硬编码所有观察者的更新逻辑,会导致强耦合——新增观察者需修改主题代码,违反“开闭原则”。
观察者模式的设计意图正是解决这一问题:
- 分离“状态持有”(主题)与“状态响应”(观察者)的职责,符合“单一职责原则”;
- 主题仅依赖观察者的抽象接口,新增观察者无需修改主题代码,符合“开闭原则”;
- 支持“广播通信”,主题状态变化时,所有注册的观察者都会被通知,无需手动触发。
二、观察者模式的核心角色
观察者模式包含4个核心角色(部分场景可扩展第5个角色),各角色职责明确,通过抽象接口实现解耦。
核心角色定义
角色名称 | 职责描述 |
---|---|
Subject(主题/被观察者) | 抽象基类,定义观察者的注册、注销、通知接口;维护一个观察者列表。 |
ConcreteSubject(具体主题) | 继承Subject,维护实际业务状态;状态变化时,调用Subject的notify 方法通知观察者。 |
Observer(观察者) | 抽象基类,定义观察者的“更新接口”(如update ),供主题通知时调用。 |
ConcreteObserver(具体观察者) | 继承Observer,实现update 方法;可持有ConcreteSubject的引用,用于获取最新状态。 |
Event(事件对象,可选) | 封装主题的状态变化细节(如“温度从25℃升至30℃”),避免update 参数过多。 |
三、C++基础实现:从抽象到具体
C++中实现观察者模式的核心是基于抽象基类定义接口,通过“组合”让主题持有观察者列表,并在状态变化时遍历通知。以下以“气象站-显示器/报警器”为例,实现完整流程。
1. 步骤1:定义Event与抽象基类
首先定义WeatherEvent
封装状态变化细节,再定义Subject
和Observer
抽象基类:
#include <vector>
#include <memory> // 智能指针,避免内存泄漏
#include <iostream>
#include <string>// 1. 事件对象:封装气象站状态变化细节
struct WeatherEvent {std::string time; // 时间戳float temperature; // 温度(℃)float humidity; // 湿度(%)// 构造函数:简化事件创建WeatherEvent(const std::string& t, float temp, float hum) : time(t), temperature(temp), humidity(hum) {}
};// 2. 观察者抽象基类:定义更新接口
class Observer {
public:virtual ~Observer() = default; // 虚析构,确保子类析构正常调用// 纯虚函数:接收事件通知(推模型)virtual void update(const WeatherEvent& event) = 0;
};// 3. 主题抽象基类:定义注册、注销、通知接口
class Subject {
public:virtual ~Subject() = default;// 注册观察者virtual void addObserver(std::shared_ptr<Observer> observer) = 0;// 注销观察者virtual void removeObserver(std::shared_ptr<Observer> observer) = 0;// 通知所有观察者virtual void notifyObservers(const WeatherEvent& event) = 0;
};
2. 步骤2:实现具体主题(气象站)
ConcreteSubject
(WeatherStation
)维护实际状态,状态更新时触发通知:
// 具体主题:气象站
class WeatherStation : public Subject {
private:// 观察者列表:使用shared_ptr自动管理内存,避免野指针std::vector<std::shared_ptr<Observer>> observers_;// 气象站当前状态(可选:也可直接通过Event传递)float current_temp_;float current_hum_;public:// 注册观察者void addObserver(std::shared_ptr<Observer> observer) override {observers_.push_back(observer);std::cout << "新增观察者,当前总数:" << observers_.size() << std::endl;}// 注销观察者(遍历查找并删除)void removeObserver(std::shared_ptr<Observer> observer) override {for (auto it = observers_.begin(); it != observers_.end(); ++it) {if (*it == observer) {observers_.erase(it);std::cout << "移除观察者,当前总数:" << observers_.size() << std::endl;break;}}}// 通知所有观察者(推模型:主动传递Event)void notifyObservers(const WeatherEvent& event) override {// 遍历所有观察者,调用update方法for (auto& observer : observers_) {observer->update(event);}}// 业务逻辑:更新气象数据并触发通知void updateWeatherData(const std::string& time, float temp, float hum) {current_temp_ = temp;current_hum_ = hum;std::cout << "\n气象站更新数据:" << time << " 温度:" << temp << "℃ 湿度:" << hum << "%" << std::endl;// 创建事件并通知观察者WeatherEvent event(time, temp, hum);notifyObservers(event);}
};
3. 步骤3:实现具体观察者
ConcreteObserver
(PhoneDisplay
、AlarmSystem
)实现update
方法,响应状态变化:
// 具体观察者1:手机显示屏
class PhoneDisplay : public Observer {
private:std::string device_name_; // 设备名称(如“小明的手机”)public:PhoneDisplay(const std::string& name) : device_name_(name) {}// 实现update:更新显示屏内容void update(const WeatherEvent& event) override {std::cout << "[" << device_name_ << "] 收到气象通知:" << std::endl;std::cout << " 时间:" << event.time << std::endl;std::cout << " 温度:" << event.temperature << "℃" << std::endl;std::cout << " 湿度:" << event.humidity << "%" << std::endl;}
};// 具体观察者2:高温报警器
class AlarmSystem : public Observer {
private:float temp_threshold_; // 温度阈值(超过则报警)public:AlarmSystem(float threshold) : temp_threshold_(threshold) {}// 实现update:超过阈值则报警void update(const WeatherEvent& event) override {if (event.temperature > temp_threshold_) {std::cout << "[高温报警器] 警报!当前温度 " << event.temperature << "℃ 超过阈值 " << temp_threshold_ << "℃!" << std::endl;} else {std::cout << "[高温报警器] 状态正常,当前温度 " << event.temperature << "℃" << std::endl;}}
};
4. 步骤4:测试程序
int main() {// 1. 创建主题(气象站)std::shared_ptr<WeatherStation> station = std::make_shared<WeatherStation>();// 2. 创建观察者std::shared_ptr<Observer> phone1 = std::make_shared<PhoneDisplay>("小明的手机");std::shared_ptr<Observer> phone2 = std::make_shared<PhoneDisplay>("小红的手机");std::shared_ptr<Observer> alarm = std::make_shared<AlarmSystem>(30.0f); // 30℃阈值// 3. 注册观察者station->addObserver(phone1);station->addObserver(phone2);station->addObserver(alarm);// 4. 模拟气象数据更新station->updateWeatherData("2024-05-20 08:00", 25.5f, 60.0f);station->updateWeatherData("2024-05-20 12:00", 32.0f, 45.0f); // 超过报警阈值// 5. 注销观察者(小红的手机)station->removeObserver(phone2);station->updateWeatherData("2024-05-20 14:00", 31.5f, 42.0f);return 0;
}
5. 输出结果
新增观察者,当前总数:1
新增观察者,当前总数:2
新增观察者,当前总数:3气象站更新数据:2024-05-20 08:00 温度:25.5℃ 湿度:60%
[小明的手机] 收到气象通知:时间:2024-05-20 08:00温度:25.5℃湿度:60%
[小红的手机] 收到气象通知:时间:2024-05-20 08:00温度:25.5℃湿度:60%
[高温报警器] 状态正常,当前温度 25.5℃气象站更新数据:2024-05-20 12:00 温度:32℃ 湿度:45%
[小明的手机] 收到气象通知:时间:2024-05-20 12:00温度:32℃湿度:45%
[小红的手机] 收到气象通知:时间:2024-05-20 12:00温度:32℃湿度:45%
[高温报警器] 警报!当前温度 32℃ 超过阈值 30℃!
移除观察者,当前总数:2气象站更新数据:2024-05-20 14:00 温度:31.5℃ 湿度:42%
[小明的手机] 收到气象通知:时间:2024-05-20 14:00温度:31.5℃湿度:42%
[高温报警器] 警报!当前温度 31.5℃ 超过阈值 30℃!
四、关键变体:推模型 vs 拉模型
观察者模式的通知方式分为两种,核心区别在于状态数据的传递方式,需根据业务场景选择。
1. 推模型(Push Model)
- 定义:主题主动将完整的状态数据(如
WeatherEvent
)推送给观察者,观察者无需主动获取。 - 优点:观察者无需依赖主题的具体类型,仅通过
Event
即可获取所需数据,耦合度低; - 缺点:若观察者仅需部分数据,会造成数据冗余;新增数据字段需修改
Event
定义。 - 适用场景:观察者需要的数据集固定、数据量小(如上述气象站案例)。
2. 拉模型(Pull Model)
- 定义:主题仅通知“状态已变化”,观察者需主动从主题中“拉取”所需数据(需持有主题引用)。
- 优点:观察者可按需拉取数据,避免冗余;新增数据字段无需修改
Event
; - 缺点:观察者需依赖主题的具体类型(需知道
WeatherStation
的getTemperature()
方法),耦合度高; - 实现修改:将
Observer::update
的参数改为Subject*
,观察者通过主题指针拉取数据:// 拉模型的Observer接口 class Observer { public:virtual ~Observer() = default;// 参数改为Subject*,观察者主动拉取数据virtual void update(Subject* subject) = 0; };// 拉模型的PhoneDisplay::update实现 void PhoneDisplay::update(Subject* subject) override {// 强制转换为具体主题(需确保类型正确,有耦合风险)WeatherStation* station = dynamic_cast<WeatherStation*>(subject);if (station) {std::cout << "[" << device_name_ << "] 拉取数据:" << std::endl;std::cout << " 温度:" << station->getCurrentTemp() << "℃" << std::endl; // 需主题提供getter} }
五、C++实战问题与解决方案
基础实现中存在3个典型问题,若不解决会导致程序崩溃或内存泄漏,需重点关注。
1. 问题1:遍历通知时的迭代器失效
场景:若观察者在update
中调用removeObserver
(如“收到通知后自动注销”),会导致vector
的迭代器失效(erase
后迭代器指向非法位置)。
解决方案:
- 方案1:使用
std::list
存储观察者(list::erase
后仅当前迭代器失效,其他迭代器仍有效); - 方案2:遍历前拷贝观察者列表,基于拷贝列表通知(原列表修改不影响遍历):
void WeatherStation::notifyObservers(const WeatherEvent& event) override {// 拷贝列表,避免遍历中修改原列表导致失效std::vector<std::shared_ptr<Observer>> temp_observers = observers_;for (auto& observer : temp_observers) {observer->update(event);} }
2. 问题2:多线程下的线程安全
场景:若主题在主线程更新状态,观察者在子线程执行update
,并发操作观察者列表(add
/remove
/notify
)会导致竞态条件(如列表元素被同时读写)。
解决方案:使用std::mutex
加锁,保护观察者列表的所有操作:
#include <mutex>class WeatherStation : public Subject {
private:std::vector<std::shared_ptr<Observer>> observers_;std::mutex mutex_; // 互斥锁,保护列表操作public:void addObserver(std::shared_ptr<Observer> observer) override {std::lock_guard<std::mutex> lock(mutex_); // 自动加锁/解锁observers_.push_back(observer);}void removeObserver(std::shared_ptr<Observer> observer) override {std::lock_guard<std::mutex> lock(mutex_);// 查找并删除...}void notifyObservers(const WeatherEvent& event) override {std::lock_guard<std::mutex> lock(mutex_);// 通知...}
};
3. 问题3:观察者销毁未注销导致内存泄漏
场景:若观察者被销毁但未从主题中注销,主题的observers_
仍持有其shared_ptr
,导致观察者内存无法释放(shared_ptr
引用计数不为0)。
解决方案:
- 方案1:观察者析构时自动注销(需主题提供
removeObserver
,且观察者持有主题引用):class PhoneDisplay : public Observer { private:std::shared_ptr<Subject> subject_; // 持有主题引用public:PhoneDisplay(const std::string& name, std::shared_ptr<Subject> subject) : device_name_(name), subject_(subject) {subject_->addObserver(shared_from_this()); // 构造时注册}~PhoneDisplay() override {// 析构时自动注销subject_->removeObserver(shared_from_this());} };
- 方案2:主题使用
std::weak_ptr
存储观察者(weak_ptr
不增加引用计数,观察者销毁后自动失效):// 主题列表改为weak_ptr std::vector<std::weak_ptr<Observer>> observers_;void WeatherStation::notifyObservers(const WeatherEvent& event) override {std::lock_guard<std::mutex> lock(mutex_);for (auto it = observers_.begin(); it != observers_.end();) {// 尝试锁定weak_ptr,判断观察者是否存活if (auto observer = it->lock()) {observer->update(event);++it;} else {// 观察者已销毁,从列表中移除it = observers_.erase(it);}} }
六、高级实现的简化与优化
C++11引入的std::function
和模板可大幅简化观察者模式代码,避免重复定义ConcreteObserver
类。
1. 技巧1:使用std::function作为观察者回调
std::function
可封装lambda、函数指针、成员函数,让观察者无需继承Observer
基类,灵活性更高:
#include <functional>// 简化的主题:观察者列表为std::function
class SimpleWeatherStation {
private:using ObserverCallback = std::function<void(const WeatherEvent&)>;std::vector<ObserverCallback> callbacks_;std::mutex mutex_;public:// 注册观察者:传入回调函数void registerObserver(ObserverCallback callback) {std::lock_guard<std::mutex> lock(mutex_);callbacks_.push_back(callback);}// 通知观察者:调用所有回调void notify(const WeatherEvent& event) {std::lock_guard<std::mutex> lock(mutex_);for (auto& callback : callbacks_) {callback(event);}}// 业务逻辑:更新数据void updateData(const std::string& time, float temp, float hum) {WeatherEvent event(time, temp, hum);notify(event);}
};// 测试:直接用lambda作为观察者
int main() {SimpleWeatherStation station;// 注册观察者1:lambda(手机显示)station.registerObserver([](const WeatherEvent& e) {std::cout << "[lambda手机] 温度:" << e.temperature << "℃" << std::endl;});// 注册观察者2:绑定成员函数(报警器)AlarmSystem alarm(30.0f);station.registerObserver(std::bind(&AlarmSystem::update, &alarm, std::placeholders::_1));// 模拟数据更新station.updateData("2024-05-20 16:00", 33.0f, 40.0f);return 0;
}
2. 技巧2:模板化主题,支持任意事件类型
通过模板让主题支持不同的Event
类型(如WeatherEvent
、LogEvent
),避免为每种事件定义单独的主题类:
template <typename EventType>
class GenericSubject {
private:using Callback = std::function<void(const EventType&)>;std::vector<Callback> callbacks_;std::mutex mutex_;public:void registerCallback(Callback callback) {std::lock_guard<std::mutex> lock(mutex_);callbacks_.push_back(callback);}void notify(const EventType& event) {std::lock_guard<std::mutex> lock(mutex_);for (auto& cb : callbacks_) {cb(event);}}
};// 使用:支持WeatherEvent和LogEvent
struct LogEvent {std::string level;std::string content;
};int main() {// 气象主题(Event为WeatherEvent)GenericSubject<WeatherEvent> weather_subject;// 日志主题(Event为LogEvent)GenericSubject<LogEvent> log_subject;return 0;
}
七、观察者模式的优缺点与适用场景
1. 优点
- 解耦主题与观察者:主题仅依赖抽象接口,观察者可独立新增/删除;
- 符合开闭原则:新增观察者无需修改主题代码;
- 支持广播通信:主题状态变化时,所有观察者自动收到通知。
2. 缺点
- 通知效率低:若观察者数量多,同步通知会阻塞主题线程;
- 循环依赖风险:若观察者与主题互相引用(如A观察B,B观察A),可能导致死锁;
- 状态变化原因不透明:观察者仅知道“状态变了”,但不知道“为什么变”。
3. 适用场景
- 一个对象变化需同步更新多个对象,且对象数量不确定(如GUI按钮点击,多个控件响应);
- 需解耦“生产者”(主题)与“消费者”(观察者),让两者独立演化(如日志系统:日志生产者 vs 文件/控制台消费者);
- 需实现“广播式”通知(如监控系统:服务器状态变化,所有监控面板更新)。
八、易混淆模式对比:观察者 vs 发布-订阅(Pub/Sub)
很多开发者将两者混淆,实则核心区别在于是否存在中间件:
对比维度 | 观察者模式(Observer) | 发布-订阅模式(Pub/Sub) |
---|---|---|
耦合度 | 主题与观察者直接依赖(通过抽象接口) | 发布者与订阅者完全解耦(无直接依赖) |
中间件 | 无(主题直接通知观察者) | 有(事件总线/消息队列,转发消息) |
通信方式 | 同步(默认)/异步 | 异步(通常基于消息队列) |
适用场景 | 单机、进程内通信(如GUI、本地监控) | 分布式系统、跨进程通信(如微服务通知) |
例如:“微信公众号”是典型的Pub/Sub——作者(发布者)发布文章到微信平台(事件总线),用户(订阅者)从平台接收消息,作者与用户无直接依赖;而“本地气象站-显示器”是观察者模式——气象站直接通知显示器,无中间件。
观察者模式是C++中构建松耦合系统的核心工具,其核心在于通过抽象接口解耦主题与观察者,并实现状态变化的自动广播。
- 核心角色:Subject(注册/注销/通知)、Observer(update)、ConcreteSubject/ConcreteObserver(业务实现);
- 通知模型:推模型(主动传数据,低耦合) vs 拉模型(按需拉数据,高耦合);
- 实战问题:迭代器失效(拷贝列表)、线程安全(加锁)、内存泄漏(自动注销/weak_ptr);
- 高级优化:
std::function
简化回调、模板支持多事件类型; - 模式边界:与Pub/Sub的区别(中间件、耦合度)。
在实际项目中,需根据业务场景选择实现方式:简单场景用基础抽象类,复杂场景用std::function+模板
,分布式场景则考虑Pub/Sub模式(如使用RabbitMQ、Kafka等消息队列)。