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

观察者模式(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等消息队列)。

相关推荐
青花瓷18 小时前
Ubuntu下OpenClaw的安装(豆包火山API版)
运维·服务器·ubuntu
Dream of maid19 小时前
Linux(下)
linux·运维·服务器
齐鲁大虾19 小时前
统信系统UOS常用命令集
linux·运维·服务器
专吃海绵宝宝菠萝屋的派大星21 小时前
使用Dify对接自己开发的mcp
java·服务器·前端
大数据新鸟21 小时前
操作系统之虚拟内存
java·服务器·网络
楠奕1 天前
CentOS7安装GoldenDB单机搭建及常见报错解决方案
linux·运维·服务器
GCTTTTTT1 天前
远程服务器走本地代理
运维·服务器
剑锋所指,所向披靡!1 天前
Linux常用指令(2)
linux·运维·服务器
做咩啊~1 天前
6.增加一个flat网段
服务器·openstack
HXQ_晴天1 天前
Linux 系统的交互式进程监控工具htop
linux·服务器·网络