目录

观察者模式的定义

观察者模式(Observer)的 GoF 定义是:定义对象间的一种一对多的依赖关系,使得每当一个对象改变状态时,所有依赖于它的对象都会得到通知并被自动更新。

这里的核心前提是”通知方向是单向的”。被观察的对象(主题)不需要知道谁在观察自己、观察者具体要做什么。它只管在自己状态改变时喊一声,谁在乎谁就做出反应。

比如一个气象站,它持续采集温度、湿度、气压。数据显示屏、天气预测面板、预警系统都依赖同一份数据。气象站不可能把每个显示设备的更新逻辑都硬编码进去,它也不应该关心每个终端拿到数据后怎么用。

观察者模式的思路是把”数据源”和”数据消费者”拆开:主题只管维护一个观察者列表,状态变了就遍历通知;观察者只管在收到通知后读取数据、执行自己的处理逻辑。两者之间通过一个抽象接口解耦,新增观察者时不需要修改主题(被订阅者)的任何代码。

观察者模式用于解决什么问题

如果不用观察者,主题(被订阅者)会直接调用每个观察者的具体方法。

比如上文提到的气象站的系统,WeatherStation 在采集数据后需要更新三个设备:温度显示屏、预测面板、预警系统。

最朴素的写法是在 WeatherStation 里硬编码三个更新调用:

class WeatherStation {
public:
    void measurementsChanged(float t, float h, float p) {
        temperatureDisplay->update(t);       // 温度屏
        forecastPanel->update(t, h, p);      // 预测面板
        alertSystem->check(t, h, p);         // 预警系统
    }
};

每加一个设备就得往 WeatherStation 里追加一行调用,删一个设备也得改 WeatherStation。设备和数据源之间是紧耦合。

更严重的问题是,每个设备的接口可能不一样:update(t)、update(t,h,p)、check(t,h,p),主题必须知道每个观察者的具体接口,加一个新设备时如果接口不同,连调用方式都写不进去。

观察者模式则把通知方案标准化。所有观察者实现同一个 update() 接口,主题不再关心具体设备名和参数个数,只对着一组抽象 Observer 逐个调用 update()。新增的设备只要实现 Observer 接口并注册自己,而主题零改动。

两种实现方式的对比如下。

观察者数量朴素写法观察者模式
33 个硬编码调用 + 3 种不同接口Observer 接口 + attach/detach/notify
1010 个硬编码调用,接口混乱循环 notify,接口统一
N(动态增减)无法实现运行时 attach/detach

观察者数量越多、变化越频繁,观察者模式的价值越大。

观察者模式 UML 类图

观察者模式涉及四个角色:Subject(主题接口)、ConcreteSubject(具体主题,持有数据)、Observer(观察者接口)、ConcreteObserver(具体观察者)。

核心机制是 Subject 持有 Observer 列表,状态变化时遍历通知,观察者在收到通知后拉取数据。

flowchart BT
    subgraph SubjectSide [主题侧]
        Subject["Subject<br/>---<br/>&lt;&lt;interface&gt;&gt; Subject<br/>---<br/>+attach(Observer) : void<br/>+detach(Observer) : void<br/>+notify() : void"]
        WeatherData["WeatherData<br/>---<br/>ConcreteSubject<br/>-temperature : float<br/>-humidity : float<br/>-pressure : float<br/>---<br/>+getTemperature() : float<br/>+getHumidity() : float<br/>+getPressure() : float<br/>+measurementsChanged() : void"]
    end

    subgraph ObserverSide [观察者侧]
        Observer["Observer<br/>---<br/>&lt;&lt;interface&gt;&gt; Observer<br/>---<br/>+update() : void"]
        CurrentDisplay["CurrentDisplay<br/>---<br/>ConcreteObserver<br/>-subject : Subject*<br/>---<br/>+update() : void<br/>+display() : void"]
        ForecastPanel["ForecastPanel<br/>---<br/>ConcreteObserver<br/>-subject : Subject*<br/>---<br/>+update() : void<br/>+display() : void"]
        AlertSystem["AlertSystem<br/>---<br/>ConcreteObserver<br/>-subject : Subject*<br/>---<br/>+update() : void<br/>+check() : void"]
    end

    WeatherData -- 继承 --> Subject
    CurrentDisplay -- 继承 --> Observer
    ForecastPanel -- 继承 --> Observer
    AlertSystem -- 继承 --> Observer
    Subject -. 持有Observer* .-> Observer
    WeatherData -- 关联 --> CurrentDisplay
    WeatherData -- 关联 --> ForecastPanel
    WeatherData -- 关联 --> AlertSystem

    style Subject fill:#fef3c7,stroke:#92400e,stroke-width:2px
    style Observer fill:#dbeafe,stroke:#1e40af,stroke-width:2px
    style WeatherData fill:#fef3c7,stroke:#92400e
    style CurrentDisplay fill:#dbeafe,stroke:#1e40af
    style ForecastPanel fill:#dbeafe,stroke:#1e40af
    style AlertSystem fill:#dbeafe,stroke:#1e40af

UML 对象交互图:工作流程

以”气象站数据更新→三个设备收到通知”为例,看一次通知是怎么传播的。

sequenceDiagram
    participant W as WeatherData(ConcreteSubject)
    participant C as CurrentDisplay(Observer)
    participant F as ForecastPanel(Observer)
    participant A as AlertSystem(Observer)

    W->>W: measurementsChanged()<br/>数据变化,触发通知
    W->>W: notify()
    Note over W: 遍历所有已注册的 Observer
    W->>C: update()
    C->>W: getTemperature()
    Note over C: 拉取自己关心的数据<br/>更新显示
    W->>F: update()
    F->>W: getTemperature() / getPressure()
    Note over F: 拉取自己关心的数据<br/>执行预测逻辑
    W->>A: update()
    A->>W: getTemperature() / getHumidity()
    Note over A: 拉取自己关心的数据<br/>评估是否触发预警

关键动作是:主题数据变化后触发 notify,遍历所有观察者逐个调用 update();每个观察者在 update() 里按需拉取主题数据,各取所需、互不干扰。主题不知道观察者具体要做什么,只管通知。

伪代码:将 UML 类转变为实现

下面把 UML 里的四个角色逐个落成代码。

以下代码为说明原理的示意性伪代码,省略头文件与 include guard 等内容。

先声明前向类,再定义 Subject 和 Observer 两个接口:

class Observer;

class Subject {
public:
    virtual ~Subject() = default;
    virtual void attach(Observer* o) = 0;
    virtual void detach(Observer* o) = 0;
    virtual void notify() = 0;
};

class Observer {
public:
    virtual ~Observer() = default;
    virtual void update() = 0;    // 主题通知入口
};

WeatherData 是 ConcreteSubject,持有数据并管理观察者列表:

class WeatherData : public Subject {
    std::vector<Observer*> observers;
    float temperature = 0;
    float humidity    = 0;
    float pressure    = 0;
public:
    void attach(Observer* o) override {
        observers.push_back(o);
    }
    void detach(Observer* o) override {
        // 实际工程中建议 erase-remove idiom
        auto it = std::find(observers.begin(), observers.end(), o);
        if (it != observers.end()) observers.erase(it);
    }
    void notify() override {
        for (auto* o : observers) {
            o->update();    // 逐个通知
        }
    }
    // 数据变化时触发通知链
    void measurementsChanged() {
        notify();
    }
    // 模拟传感器更新数据
    void setMeasurements(float t, float h, float p) {
        temperature = t; humidity = h; pressure = p;
        measurementsChanged();
    }
    // 观察者通过这些 getter 拉取数据
    float getTemperature() const { return temperature; }
    float getHumidity()    const { return humidity; }
    float getPressure()    const { return pressure; }
};

CurrentDisplay 是 ConcreteObserver,update() 里拉取温度数据:

class CurrentDisplay : public Observer {
    Subject* subject;    // 保存主题引用,用于拉取数据
    float temperature = 0;
public:
    explicit CurrentDisplay(Subject* s) : subject(s) {
        subject->attach(this);    // 构造时自动注册
    }
    void update() override {
        // 从主题拉取自己关心的数据
        auto* wd = dynamic_cast<WeatherData*>(subject);
        if (wd) temperature = wd->getTemperature();
        display();
    }
    void display() const {
        // 输出当前温度
    }
};

ForecastPanel 拉取温度和气压做预测:

class ForecastPanel : public Observer {
    Subject* subject;
    float temperature = 0;
    float pressure    = 0;
public:
    explicit ForecastPanel(Subject* s) : subject(s) {
        subject->attach(this);
    }
    void update() override {
        auto* wd = dynamic_cast<WeatherData*>(subject);
        if (wd) {
            temperature = wd->getTemperature();
            pressure    = wd->getPressure();
        }
        display();
    }
    void display() const {
        // 根据温度和气压做预测
    }
};

AlertSystem 监测温度和湿度,超阈值告警:

class AlertSystem : public Observer {
    Subject* subject;
    float temperature = 0;
    float humidity    = 0;
public:
    explicit AlertSystem(Subject* s) : subject(s) {
        subject->attach(this);
    }
    void update() override {
        auto* wd = dynamic_cast<WeatherData*>(subject);
        if (wd) {
            temperature = wd->getTemperature();
            humidity    = wd->getHumidity();
        }
        check();
    }
    void check() const {
        if (temperature > 40.0f) {
            // 高温预警
        }
    }
};

Client 端组装主题和观察者,数据更新自动推送给所有设备:

int main() {
    WeatherData weather;

    CurrentDisplay  display(&weather);
    ForecastPanel   forecast(&weather);
    AlertSystem     alert(&weather);

    weather.setMeasurements(25.0f, 65.0f, 1013.0f);
    // 三个观察者的 update() 自动被调用
    // display 显示温度 25.0
    // forecast 用温度 25.0 + 气压 1013.0 做预测
    // alert 检查温度 25.0 未超阈值

    weather.setMeasurements(42.0f, 30.0f, 1008.0f);
    // alert 触发高温预警
}

以上代码走通了完整的通知链:主题数据变化→notify→遍历所有观察者→每个观察者 update 里拉取数据做自己的事。新增一个观察者,只要实现 Observer 并注册到主题即可。

用 std::function 的轻量替代

如果观察者数量不多、逻辑简单,可以用 std::function 回调代替整套接口继承。

class WeatherStation {
    std::vector<std::function<void(float, float, float)>> callbacks;
public:
    void subscribe(std::function<void(float, float, float)> cb) {
        callbacks.push_back(std::move(cb));
    }
    void notify(float t, float h, float p) {
        for (auto& cb : callbacks) {
            cb(t, h, p);
        }
    }
};

// 使用端
WeatherStation ws;

ws.subscribe([](float t, float h, float p) {
    std::cout << "当前温度: " << t << "\n";
});

ws.subscribe([threshold = 40.0f](float t, float, float) {
    if (t > threshold) std::cout << "高温预警!\n";
});

这种写法省掉了 Observer 接口、继承和 dynamic_cast,回调直接接收数据参数,代码量少很多。

代价是缺乏生命周期管理。observer 对象如果被销毁,std::function 回调向量里的 lambda 捕获的裸指针就成了悬垂引用。经典 Observer 模式可以靠 detach/unregister 机制保护这一点,而回调方案需要自己处理。两种写法没有绝对优劣,取决于你的场景:观察者数量多、生命周期复杂、需要运行时增删,用经典模式;逻辑简单、生命周期可控、追求代码紧凑,用 std::function。

落到工程里要注意的几点

Subject 的析构函数要小心。 如果主题持有观察者的裸指针,观察者被销毁时没有 detach,主题的列表里就留下悬垂指针,下次 notify 会访问已释放内存。反过来,观察者持有主题的裸指针,主题先于观察者被销毁时,观察者后续调用 getter 也会访问已释放内存。实际工程通常用 std::weak_ptr 解决循环引用,或强制观察者析构前 detach。

notify 过程中不要增删观察者。 如果某个观察者的 update() 里触发了 attach 或 detach,当前正在遍历的 vector 被修改,迭代器失效导致崩溃。解决办法是 notify 时拷贝一份观察者列表再遍历,或者使用 std::list + 标记删除。

推模型 vs 拉模型需要权衡。 push 模型是主题在 notify 时直接把数据传给观察者(update(t, h, p)),观察者不需要反向拉取,效率高但假设所有观察者都要同样的数据。pull 模型是本文示例的做法(update() 无参数,观察者主动 getter),灵活但多一次回调。选哪种取决于数据一致性要求和观察者类型多少。

dynamic_cast 是把双刃剑。 本文伪代码里 ConcreteObserver 用 dynamic_cast<WeatherData*> 获取主题引用,这在观察者和主题强绑定时不优雅。更好的做法是给 Observer 构造函数直接传 WeatherData*,省掉 cast。另一个方向是让 Subject 的 notify 接口把自身引用作为参数传进去(update(Subject*)),观察者自己决定要不要转成具体类型。

线程安全需要额外处理。 如果 notify 发生在某条线程,而另一条线程正在 attach/detach,不加锁就是 data race。最轻量的方案是 std::shared_mutex(C++17):attach/detach 加写锁,notify 加读锁。