观察者模式详解:实现对象间的"一对多"通知,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>

// 前向声明
class Observer;

// 抽象主题:天气主题
class WeatherSubject {
public:
    virtual ~WeatherSubject() = default;
    // 注册观察者
    virtual void attach(std::shared_ptr<Observer> observer) = 0;
    // 移除观察者
    virtual void detach(std::shared_ptr<Observer> observer) = 0;
    // 通知所有观察者
    virtual void notifyObservers() = 0;
};

// 抽象观察者
class Observer {
public:
    virtual ~Observer() = default;
    // 更新方法,由主题调用
    virtual void update(float temperature, float humidity, float pressure) = 0;
};

// 具体主题:天气数据
class WeatherData : public WeatherSubject {
private:
    std::vector<std::shared_ptr<Observer>> observers_; // 观察者列表
    float temperature_; // 温度
    float humidity_;    // 湿度
    float pressure_;    // 气压

public:
    // 注册观察者
    void attach(std::shared_ptr<Observer> observer) override {
        observers_.push_back(observer);
    }

    // 移除观察者
    void detach(std::shared_ptr<Observer> observer) override {
        auto it = std::find(observers_.begin(), observers_.end(), observer);
        if (it != observers_.end()) {
            observers_.erase(it);
        }
    }

    // 通知所有观察者
    void notifyObservers() override {
        // 复制一份观察者列表,避免在通知过程中列表被修改导致迭代器失效
        auto observers_copy = observers_;
        for (const auto& observer : observers_copy) {
            observer->update(temperature_, humidity_, pressure_);
        }
    }

    // 当天气数据更新时调用
    void measurementsChanged() {
        notifyObservers();
    }

    // 设置天气数据
    void setMeasurements(float temperature, float humidity, float pressure) {
        temperature_ = temperature;
        humidity_ = humidity;
        pressure_ = pressure;
        measurementsChanged();
    }

    // 获取天气数据(拉模式时使用)
    float getTemperature() const { return temperature_; }
    float getHumidity() const { return humidity_; }
    float getPressure() const { return pressure_; }
};

// 具体观察者1:当前天气显示
class CurrentConditionsDisplay : public Observer {
private:
    float temperature_;
    float humidity_;
    std::shared_ptr<WeatherData> weather_data_;

public:
    explicit CurrentConditionsDisplay(std::shared_ptr<WeatherData> weather_data)
        : weather_data_(weather_data) {
        // 注册自己到主题
        weather_data_->attach(shared_from_this());
    }

    // 更新显示
    void update(float temperature, float humidity, float pressure) override {
        temperature_ = temperature;
        humidity_ = humidity;
        display();
    }

    // 显示当前天气
    void display() const {
        std::cout << "当前天气: 温度 " << temperature_ << "°C, 湿度 " << humidity_ << "%" << std::endl;
    }
};

// 具体观察者2:天气统计显示
class StatisticsDisplay : public Observer {
private:
    float max_temp_ = -100.0f;
    float min_temp_ = 100.0f;
    float sum_temp_ = 0.0f;
    int count_ = 0;
    std::shared_ptr<WeatherData> weather_data_;

public:
    explicit StatisticsDisplay(std::shared_ptr<WeatherData> weather_data)
        : weather_data_(weather_data) {
        weather_data_->attach(shared_from_this());
    }

    void update(float temperature, float humidity, float pressure) override {
        sum_temp_ += temperature;
        count_++;

        if (temperature > max_temp_) max_temp_ = temperature;
        if (temperature < min_temp_) min_temp_ = temperature;

        display();
    }

    void display() const {
        std::cout << "天气统计: 平均温度 " << sum_temp_ / count_ 
                  << "°C, 最高温度 " << max_temp_ 
                  << "°C, 最低温度 " << min_temp_ << "°C" << std::endl;
    }
};

// 具体观察者3:天气预报显示
class ForecastDisplay : public Observer {
private:
    float current_pressure_ = 1013.0f;
    float last_pressure_;
    std::shared_ptr<WeatherData> weather_data_;

public:
    explicit ForecastDisplay(std::shared_ptr<WeatherData> weather_data)
        : weather_data_(weather_data) {
        weather_data_->attach(shared_from_this());
    }

    void update(float temperature, float humidity, float pressure) override {
        last_pressure_ = current_pressure_;
        current_pressure_ = pressure;
        display();
    }

    void display() const {
        std::cout << "天气预报: ";
        if (current_pressure_ > last_pressure_) {
            std::cout << "气压上升,天气将转晴" << std::endl;
        } else if (current_pressure_ < last_pressure_) {
            std::cout << "气压下降,可能会下雨" << std::endl;
        } else {
            std::cout << "气压稳定,天气保持不变" << std::endl;
        }
    }
};

// 客户端代码
int main() {
    // 创建天气数据主题
    auto weather_data = std::make_shared<WeatherData>();

    // 创建观察者并注册到主题
    auto current_display = std::make_shared<CurrentConditionsDisplay>(weather_data);
    auto statistics_display = std::make_shared<StatisticsDisplay>(weather_data);
    auto forecast_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);

    return 0;
}

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 拉模式

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

拉模式实现示意

// 抽象观察者(拉模式)
class Observer {
public:
    virtual ~Observer() = default;
    // 拉模式下update方法不接收参数
    virtual void update() = 0;
};

// 具体观察者(拉模式)
class CurrentConditionsDisplay : public Observer {
private:
    std::shared_ptr<WeatherData> weather_data_;

public:
    explicit CurrentConditionsDisplay(std::shared_ptr<WeatherData> weather_data)
        : weather_data_(weather_data) {
        weather_data_->attach(shared_from_this());
    }

    void update() override {
        // 主动从主题拉取需要的数据
        float temp = weather_data_->getTemperature();
        float humidity = weather_data_->getHumidity();
        std::cout << "当前天气: 温度 " << temp << "°C, 湿度 " << humidity << "%" << std::endl;
    }
};

// 具体主题的notifyObservers方法(拉模式)
void notifyObservers() override {
    auto observers_copy = observers_;
    for (const auto& 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++风格的主题类
class WeatherStation {
private:
    // 使用std::function作为观察者类型
    using Observer = std::function<void(float, float, float)>;
    std::vector<Observer> observers_;
    float temperature_;
    float humidity_;
    float pressure_;

public:
    // 注册观察者
    void subscribe(Observer observer) {
        observers_.push_back(std::move(observer));
    }

    // 通知所有观察者
    void notify() {
        for (const auto& observer : observers_) {
            observer(temperature_, humidity_, pressure_);
        }
    }

    // 设置天气数据
    void setMeasurements(float temp, float humidity, float pressure) {
        temperature_ = temp;
        humidity_ = humidity;
        pressure_ = pressure;
        notify();
    }
};

// 客户端代码
int main() {
    WeatherStation station;

    // 使用Lambda表达式作为观察者
    station.subscribe([](float temp, float humidity, float pressure) {
        std::cout << "Lambda观察者: 温度 " << temp << "°C" << std::endl;
    });

    station.subscribe([](float temp, float humidity, float pressure) {
        std::cout << "Lambda观察者: 湿度 " << humidity << "%" << std::endl;
    });

    station.setMeasurements(25.0f, 60.0f, 1013.0f);

    return 0;
}

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

6.2 通用事件总线

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

#include <any>
#include <unordered_map>
#include <typeindex>

class EventBus {
private:
    using Handler = std::function<void(const std::any&)>;
    std::unordered_map<std::type_index, std::vector<Handler>> handlers_;

public:
    // 订阅事件
    template <typename EventType>
    void subscribe(std::function<void(const EventType&)> handler) {
        handlers_[std::type_index(typeid(EventType))].push_back(
            [handler = std::move(handler)](const std::any& event) {
                handler(std::any_cast<const EventType&>(event));
            }
        );
    }

    // 发布事件
    template <typename EventType>
    void publish(const EventType& event) {
        auto it = handlers_.find(std::type_index(typeid(EventType)));
        if (it != handlers_.end()) {
            for (const auto& handler : it->second) {
                handler(event);
            }
        }
    }
};

// 定义事件类型
struct TemperatureChangedEvent {
    float new_temperature;
};

struct HumidityChangedEvent {
    float new_humidity;
};

// 客户端代码
int main() {
    EventBus bus;

    bus.subscribe<TemperatureChangedEvent>([](const TemperatureChangedEvent& e) {
        std::cout << "温度更新: " << e.new_temperature << "°C" << std::endl;
    });

    bus.subscribe<HumidityChangedEvent>([](const HumidityChangedEvent& e) {
        std::cout << "湿度更新: " << e.new_humidity << "%" << std::endl;
    });

    bus.publish(TemperatureChangedEvent{25.5f});
    bus.publish(HumidityChangedEvent{65.0f});

    return 0;
}

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


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

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

八、总结

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

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

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

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

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

更多推荐