设计模式---观察者模式

观察者模式(Observer Pattern)是行为型设计模式 的核心成员,其核心思想是定义对象间的一对多依赖关系 ------当"被观察者"(主题)的状态发生变化时,所有依赖它的"观察者"对象会自动收到通知并执行更新操作。这种模式的本质是解耦主题与观察者,让两者可以独立演化,同时保证状态变化的"广播式"传递。在C++程序设计中,观察者模式广泛应用于GUI事件处理、监控系统、日志订阅、消息通知等场景,是构建松耦合系统的关键工具。

一、观察者模式的核心定义与设计意图

在软件设计中,经常会遇到"一个对象变化导致多个对象需要同步更新"的场景(例如:气象站数据变化时,手机APP、显示屏、报警器需同时更新)。若直接在主题中硬编码所有观察者的更新逻辑,会导致强耦合------新增观察者需修改主题代码,违反"开闭原则"。

观察者模式的设计意图正是解决这一问题:

  1. 分离"状态持有"(主题)与"状态响应"(观察者)的职责,符合"单一职责原则";
  2. 主题仅依赖观察者的抽象接口,新增观察者无需修改主题代码,符合"开闭原则";
  3. 支持"广播通信",主题状态变化时,所有注册的观察者都会被通知,无需手动触发。

二、观察者模式的核心角色

观察者模式包含4个核心角色(部分场景可扩展第5个角色),各角色职责明确,通过抽象接口实现解耦。

核心角色定义

角色名称 职责描述
Subject(主题/被观察者) 抽象基类,定义观察者的注册、注销、通知接口;维护一个观察者列表。
ConcreteSubject(具体主题) 继承Subject,维护实际业务状态;状态变化时,调用Subject的notify方法通知观察者。
Observer(观察者) 抽象基类,定义观察者的"更新接口"(如update),供主题通知时调用。
ConcreteObserver(具体观察者) 继承Observer,实现update方法;可持有ConcreteSubject的引用,用于获取最新状态。
Event(事件对象,可选) 封装主题的状态变化细节(如"温度从25℃升至30℃"),避免update参数过多。

三、C++基础实现:从抽象到具体

C++中实现观察者模式的核心是基于抽象基类定义接口,通过"组合"让主题持有观察者列表,并在状态变化时遍历通知。以下以"气象站-显示器/报警器"为例,实现完整流程。

1. 步骤1:定义Event与抽象基类

首先定义WeatherEvent封装状态变化细节,再定义SubjectObserver抽象基类:

cpp 复制代码
#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:实现具体主题(气象站)

ConcreteSubjectWeatherStation)维护实际状态,状态更新时触发通知:

cpp 复制代码
// 具体主题:气象站
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:实现具体观察者

ConcreteObserverPhoneDisplayAlarmSystem)实现update方法,响应状态变化:

cpp 复制代码
// 具体观察者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:测试程序

cpp 复制代码
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

  • 缺点 :观察者需依赖主题的具体类型(需知道WeatherStationgetTemperature()方法),耦合度高;

  • 实现修改 :将Observer::update的参数改为Subject*,观察者通过主题指针拉取数据:

    cpp 复制代码
    // 拉模型的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:遍历前拷贝观察者列表,基于拷贝列表通知(原列表修改不影响遍历):

    cpp 复制代码
    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加锁,保护观察者列表的所有操作:

cpp 复制代码
#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,且观察者持有主题引用):

    cpp 复制代码
    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不增加引用计数,观察者销毁后自动失效):

    cpp 复制代码
    // 主题列表改为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基类,灵活性更高:

cpp 复制代码
#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类型(如WeatherEventLogEvent),避免为每种事件定义单独的主题类:

cpp 复制代码
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++中构建松耦合系统的核心工具,其核心在于通过抽象接口解耦主题与观察者,并实现状态变化的自动广播。

  1. 核心角色:Subject(注册/注销/通知)、Observer(update)、ConcreteSubject/ConcreteObserver(业务实现);
  2. 通知模型:推模型(主动传数据,低耦合) vs 拉模型(按需拉数据,高耦合);
  3. 实战问题:迭代器失效(拷贝列表)、线程安全(加锁)、内存泄漏(自动注销/weak_ptr);
  4. 高级优化:std::function简化回调、模板支持多事件类型;
  5. 模式边界:与Pub/Sub的区别(中间件、耦合度)。

在实际项目中,需根据业务场景选择实现方式:简单场景用基础抽象类,复杂场景用std::function+模板,分布式场景则考虑Pub/Sub模式(如使用RabbitMQ、Kafka等消息队列)。

相关推荐
wangjialelele4 小时前
端口号、常见协议和套接字
linux·运维·服务器·c语言·网络
蜜蜜不吃糖4 小时前
ESXI主机重置带外密码
linux·运维·服务器
王道长服务器 | 亚马逊云4 小时前
AWS CloudTrail:让每一次操作都“有迹可循”
服务器·网络·云计算·智能路由器·aws
半夏知半秋5 小时前
lua对象池管理工具剖析
服务器·开发语言·后端·学习·lua
wanhengidc5 小时前
操作简单稳定选巨 椰 云手机
运维·服务器·游戏·智能手机·云计算
wanhengidc5 小时前
云手机公认的优势有什么
运维·服务器·游戏·智能手机·玩游戏
一匹电信狗5 小时前
【C++】C++风格的类型转换
服务器·开发语言·c++·leetcode·小程序·stl·visual studio
老龄程序员6 小时前
基于OpenIddict6.4.0搭建授权认证服务
运维·服务器·identityserver
DechinPhy7 小时前
Ubuntu挂载新硬盘
linux·运维·服务器·ubuntu