90、STL与观察者模式:事件通知、订阅机制、松耦合

观察者模式,说白了就是「你关注我,我变了就告诉你」。

我在做游戏引擎的时候,经常遇到这种场景:玩家血量变了,UI要更新、音效要播放、成就系统要检查。如果每个模块都直接去轮询血量,代码会乱成一锅粥。观察者模式就是解决这个问题的——它让对象之间保持松耦合,一个对象状态变了,所有依赖它的对象都能自动收到通知。

观察者模式的核心思想

观察者模式有两个核心角色:

  • 主题(Subject):维护一个观察者列表,状态变化时通知所有观察者。
  • 观察者(Observer):注册到主题上,收到通知后执行自己的逻辑。

你想想看,这就像公众号和订阅者。公众号发文章,所有订阅者都能收到。公众号不需要知道订阅者是谁,订阅者也不需要关心公众号怎么运作。这就是松耦合。

关键点:观察者模式的核心是「推」而不是「拉」。主题主动推送事件,观察者被动接收。这避免了轮询带来的性能浪费和代码耦合。

STL如何助力观察者模式

STL虽然没有直接提供观察者模式的类,但它的容器和算法可以帮我们优雅地实现这个模式。我个人习惯用 std::vectorstd::list 来存储观察者,用 std::function 来封装回调函数。

为什么用 std::function?因为它可以接受函数指针、lambda表达式、函数对象,灵活性极高。我在项目中遇到过需要同时支持普通函数和成员函数的情况,std::function 一把搞定。

一个完整的事件通知系统

下面我写一个简单但完整的事件通知系统。假设我们有一个「玩家血量变化」的事件,多个模块需要响应。

#include <iostream>
#include <vector>
#include <functional>
#include <string>
#include <algorithm>

// 事件参数结构体
struct HealthEvent {
    int oldHealth;
    int newHealth;
    std::string playerName;
};

// 主题类
class HealthSubject {
private:
    // 观察者列表:存储回调函数
    std::vector<std::function<void(const HealthEvent&)>> observers;

public:
    // 订阅事件
    void subscribe(std::function<void(const HealthEvent&)> callback) {
        observers.push_back(callback);
    }

    // 取消订阅
    void unsubscribe(const std::function<void(const HealthEvent&)>& callback) {
        // 这里用 remove-erase 惯用法
        auto it = std::remove_if(observers.begin(), observers.end(),
            [&](const std::function<void(const HealthEvent&)>& obs) {
                // 注意:std::function 不支持直接比较
                // 实际项目中可以用 token 或 ID 来标识
                return false; // 简化处理
            });
        // observers.erase(it, observers.end());
    }

    // 通知所有观察者
    void notify(const HealthEvent& event) {
        for (auto& observer : observers) {
            observer(event);
        }
    }
};

// 观察者:UI模块
class UIModule {
public:
    void onHealthChanged(const HealthEvent& event) {
        std::cout << "[UI] 血量从 " << event.oldHealth
                  << " 变为 " << event.newHealth << std::endl;
        if (event.newHealth < 20) {
            std::cout << "[UI] 警告:血量过低!" << std::endl;
        }
    }
};

// 观察者:音效模块
class AudioModule {
public:
    void onHealthChanged(const HealthEvent& event) {
        if (event.newHealth < event.oldHealth) {
            std::cout << "[Audio] 播放受伤音效" << std::endl;
        } else {
            std::cout << "[Audio] 播放治疗音效" << std::endl;
        }
    }
};

int main() {
    HealthSubject subject;

    UIModule ui;
    AudioModule audio;

    // 使用 lambda 绑定成员函数
    subject.subscribe([&ui](const HealthEvent& e) {
        ui.onHealthChanged(e);
    });

    subject.subscribe([&audio](const HealthEvent& e) {
        audio.onHealthChanged(e);
    });

    // 模拟血量变化
    HealthEvent event1{100, 80, "Player1"};
    subject.notify(event1);

    std::cout << "--- 第二次变化 ---" << std::endl;
    HealthEvent event2{80, 15, "Player1"};
    subject.notify(event2);

    return 0;
}

运行结果:

[UI] 血量从 100 变为 80
[Audio] 播放受伤音效
--- 第二次变化 ---
[UI] 血量从 80 变为 15
[UI] 警告:血量过低!
[Audio] 播放受伤音效

小技巧:用 lambda 绑定成员函数时,注意捕获 this 指针或对象的引用。如果对象可能被销毁,记得用 std::weak_ptr 来避免悬空指针。

观察者模式的核心流程图

下面这张图展示了整个事件通知的流程:

主题 (Subject) 维护观察者列表 观察者1 (UI) 更新血量显示 观察者2 (音效) 播放受伤/治疗音效 观察者3 (成就) 检查成就解锁 事件:血量变化 (HealthEvent) notify() 通知所有观察者 状态变化触发事件 subscribe() subscribe() subscribe()

流程很简单:观察者先通过 subscribe() 注册到主题上。当主题状态变化时,调用 notify() 遍历观察者列表,逐个调用回调函数。每个观察者收到事件后,执行自己的逻辑。

松耦合的好处

观察者模式最大的价值就是松耦合。主题不需要知道观察者的具体类型,观察者也不需要知道主题的内部实现。它们只通过事件接口通信。

我曾经在一个项目中接手过一段代码,里面全是硬编码的调用关系:ui->update()audio->play()achievement->check() 写在一起。后来需求变了,要加一个日志模块,得改好几个文件。改成观察者模式后,新增模块只需要订阅事件,一行代码都不用改原来的逻辑。

避坑指南:我曾经遇到过一个问题——观察者回调中又触发了事件,导致无限递归。比如在血量变化回调中又修改了血量。解决方案是加一个「正在通知」的标志位,或者在事件队列中延迟处理。

进阶:带优先级的事件系统

有时候我们希望某些观察者先收到通知。比如日志模块应该最后执行,UI模块应该优先更新。可以用 std::multimap 来存储带优先级的观察者:

#include <map>
#include <functional>

class PrioritySubject {
private:
    // 用 multimap 存储:key 是优先级,value 是回调
    std::multimap<int, std::function<void(const HealthEvent&)>> observers;

public:
    void subscribe(int priority, std::function<void(const HealthEvent&)> callback) {
        observers.insert({priority, callback});
    }

    void notify(const HealthEvent& event) {
        // multimap 默认按 key 升序排列
        for (auto& [priority, callback] : observers) {
            callback(event);
        }
    }
};

这样,优先级高的观察者(数值小)会先收到通知。我在做UI框架时就用过这个技巧,让渲染模块先更新,日志模块最后记录。

总结

观察者模式是解耦的利器,STL 的 std::function 和容器让实现变得异常简单。记住几个要点:

  • std::vectorstd::list 存储回调
  • std::function 封装任意可调用对象
  • 注意避免循环触发和悬空指针
  • 需要优先级时用 std::multimap

嗯,这个模式在实际项目中太常用了。你想想看,从游戏引擎到GUI框架,从消息总线到插件系统,到处都有它的影子。掌握了它,你的代码会变得更灵活、更易维护。

核心要点:观察者模式 = 事件驱动 + 松耦合 + 自动通知。STL 提供了基础设施,你只需要搭好架子。


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