22. 观察者模式:模式动机与定义、角色与结构、C++代码实现、事件驱动编程

观察者模式,说白了就是「发布-订阅」机制。一个对象状态变了,其他一堆对象能自动收到通知。我早年做的一个监控系统,服务器挂了,客户端得立刻知道——那时候用的就是观察者模式。今天咱们把它掰开揉碎讲清楚。

22.1 模式动机与定义

先想想一个场景:你写了个股票行情软件,股价一变,所有展示面板(K线图、数字报价、新闻推送)都得刷新。如果每个面板都去轮询股价,CPU 得炸。更好的做法是:股价中心一变化,主动通知所有面板。

这就是观察者模式的核心动机——解耦。被观察者(Subject)不需要知道观察者(Observer)是谁,只需要知道它们实现了同一个接口。观察者可以随时加入或退出,不影响系统其他部分。

定义:观察者模式定义了对象之间的一对多依赖关系,当一个对象状态发生变化时,所有依赖它的对象都会得到通知并自动更新。

我个人习惯把观察者模式理解为「报社与订户」的关系。报社只管印报纸,订户只管收报纸。订户多了少了,报社不用管。你想想看,这种松耦合的设计,在大型系统中有多重要。

22.2 角色与结构

观察者模式涉及四个角色,我画了张图帮你理清关系:

Subject(抽象主题) + attach(Observer*) + detach(Observer*) + notify() ConcreteSubject - state: int + getState(): int + setState(int) Observer(抽象观察者) + update(Subject*) ConcreteObserver - state: int + update(Subject*) 观察者列表 观察者模式结构图 Subject 维护观察者列表,状态变化时自动通知所有 Observer setState() → notify() → 遍历 Observer::update()

角色说明:

  • Subject(抽象主题):提供 attach、detach、notify 三个核心方法。它维护一个观察者列表。
  • ConcreteSubject(具体主题):持有真正的状态数据。状态变了就调 notify()。
  • Observer(抽象观察者):定义 update 接口,所有观察者必须实现它。
  • ConcreteObserver(具体观察者):实现 update,收到通知后拉取数据或做自己的事。

我的经验:实际项目中,Subject 的 notify() 调用时机很关键。我见过有人把 notify 放在 setter 里,结果构造函数里调 setter 时,观察者还没注册完,直接崩了。建议:状态批量修改完,手动调一次 notify,别在 setter 里自动触发。

22.3 C++代码实现

直接上代码。我写了一个股票报价的例子,你一看就懂。

#include <iostream>
#include <vector>
#include <algorithm>
#include <memory>

// 前向声明
class Subject;

// 抽象观察者
class Observer {
public:
    virtual ~Observer() = default;
    virtual void update(Subject* subject) = 0;
};

// 抽象主题
class Subject {
public:
    virtual ~Subject() = default;
    
    void attach(Observer* obs) {
        observers_.push_back(obs);
    }
    
    void detach(Observer* obs) {
        auto it = std::find(observers_.begin(), observers_.end(), obs);
        if (it != observers_.end()) {
            observers_.erase(it);
        }
    }
    
    void notify() {
        for (auto* obs : observers_) {
            obs->update(this);
        }
    }
    
private:
    std::vector<Observer*> observers_;
};

// 具体主题:股票价格
class StockPrice : public Subject {
public:
    void setPrice(double price) {
        price_ = price;
        notify();  // 价格变了,通知所有人
    }
    
    double getPrice() const { return price_; }
    
private:
    double price_ = 0.0;
};

// 具体观察者:散户
class RetailInvestor : public Observer {
public:
    explicit RetailInvestor(const std::string& name) : name_(name) {}
    
    void update(Subject* subject) override {
        auto* stock = dynamic_cast<StockPrice*>(subject);
        if (stock) {
            std::cout << "[" << name_ << "] 收到通知,当前股价: "
                      << stock->getPrice() << std::endl;
        }
    }
    
private:
    std::string name_;
};

// 具体观察者:量化交易系统
class QuantSystem : public Observer {
public:
    void update(Subject* subject) override {
        auto* stock = dynamic_cast<StockPrice*>(subject);
        if (stock) {
            double price = stock->getPrice();
            if (price < 100.0) {
                std::cout << "[量化系统] 价格低于100,触发买入策略!" << std::endl;
            }
        }
    }
};

int main() {
    StockPrice apple;
    
    RetailInvestor alice("Alice");
    RetailInvestor bob("Bob");
    QuantSystem quant;
    
    apple.attach(&alice);
    apple.attach(&bob);
    apple.attach(&quant);
    
    apple.setPrice(150.0);
    apple.setPrice(95.0);  // 量化系统会触发买入
    
    apple.detach(&bob);
    apple.setPrice(120.0);  // Bob 不再收到通知
    
    return 0;
}

运行结果:

[Alice] 收到通知,当前股价: 150
[Bob] 收到通知,当前股价: 150
[量化系统] 价格低于100,触发买入策略!
[Alice] 收到通知,当前股价: 120
[量化系统] 收到通知,当前股价: 120

注意:上面的代码用了原始指针管理观察者。我曾经在一个项目中因为观察者被提前销毁但没 detach,导致野指针崩溃。建议用 std::shared_ptr 或确保析构时自动 detach。另外,notify 里遍历观察者时,如果某个观察者的 update 又调了 attach/detach,会迭代器失效。可以用「先拷贝列表再遍历」的方式规避。

22.4 事件驱动编程

观察者模式是事件驱动编程的基石。你想想看,GUI 里的按钮点击、网络库里的连接建立、游戏里的碰撞检测——本质上都是观察者模式。

事件驱动编程的核心思想:不主动轮询,而是被动响应。系统维护一个事件循环,事件来了,分发给对应的处理器。观察者模式正好提供了这种「注册-通知」的机制。

我参与过一个实时交易系统,每秒处理上万笔订单。如果用轮询,CPU 全浪费在空转上。改用事件驱动后,每个订单处理完就触发「订单状态变更事件」,风控模块、清算模块、报表模块各自注册监听,效率提升了一个数量级。

事件驱动编程的几个关键点:

  • 事件源:谁产生事件?比如用户点击、网络数据到达。
  • 事件对象:携带什么数据?比如鼠标坐标、键盘按键码。
  • 事件处理器:谁处理事件?就是观察者。
  • 事件循环:谁分发事件?通常是框架或底层库。

实战建议:在 C++ 中实现事件驱动,可以用 std::function 替代纯虚接口。这样更灵活,观察者不需要继承某个基类。比如:

class EventEmitter {
public:
    using Handler = std::function<void(const Event&)>;
    
    void on(const std::string& eventName, Handler handler) {
        handlers_[eventName].push_back(std::move(handler));
    }
    
    void emit(const std::string& eventName, const Event& event) {
        auto it = handlers_.find(eventName);
        if (it != handlers_.end()) {
            for (auto& handler : it->second) {
                handler(event);
            }
        }
    }
    
private:
    std::unordered_map<std::string, std::vector<Handler>> handlers_;
};

这种写法去掉了 Observer 基类,用 std::function 做回调。我更喜欢这种方式,因为它更轻量,而且 lambda 表达式用起来很顺手。

避坑指南:事件驱动编程容易踩的坑是「回调地狱」——事件里又触发事件,形成循环。我曾经调试过一个死循环,找了半天发现是 A 事件处理里发了 B 事件,B 事件处理里又发了 A 事件。解决方案:加递归深度限制,或者用异步队列延迟处理。

总结一下:观察者模式是解耦的利器,事件驱动编程是它的高级形态。从 GUI 到后端服务,从游戏到金融系统,这套思想无处不在。你写代码时多想想「谁依赖谁」,自然就能用好它。


公众号:蓝海资料掘金营,微信deep3321