导读:本期聚焦于小伙伴创作的《C++怎么实现一个观察者模式?事件通知与解耦机制详解》,敬请观看详情。直接把业务逻辑写死在状态变更函数里,往往会让模块之间产生难以维护的硬依赖。观察者模式通过抽象出发布者与订阅者角色,使对象在状态变化时可自动通知其他对象而无需知晓对方细节。在C++中可利用抽象基类定义观察者接口,由具体观察者实现更新逻辑,主题类维护观察者容器并在事件发生时遍历调用。相比直接在代码里调用各个处理函数,这种机制把通知逻辑与业务响应分离,新增功能只需注册新观察者。标准库signal、function与智能指针能进一步简化内存与生命周期管理,避免裸指针带来的悬空问题。

在大型C++项目里,一个模块的状态变化常常需要触发其他多个模块的响应,比如界面刷新、日志记录、网络同步等。如果让状态持有者直接调用这些响应函数,代码会迅速变成一张错综复杂的调用网,任何功能增减都要改动核心逻辑。观察者模式正是为了解决这类耦合问题而诞生的设计模式,它定义了对象间一对多的依赖关系,让主题在状态改变时自动通知所有依赖者。

C++怎么实现一个观察者模式?事件通知与解耦机制详解

一、观察者模式的核心角色与原理

观察者模式包含两种核心角色:主题(Subject)和观察者(Observer)。主题负责维护一个观察者列表,并提供注册、注销以及通知的接口;观察者则定义一个统一的更新接口,具体观察者实现该接口以响应主题通知。这样一来,主题不需要知道观察者的具体类型,只依赖抽象接口,从而实现了编译期和运行期的解耦。

从底层原理看,这种模式本质是对回调机制的规范化。主题内部保存的是指向观察者接口的指针或引用,当事件发生时,主题遍历列表并调用接口方法。由于接口是稳定的,新增观察者不会破坏主题代码,符合开闭原则。在C++中,我们通常用抽象类或结构体来定义观察者接口,用标准容器管理观察者,配合智能指针规避资源泄漏。

1.1 基础接口设计

我们先定义一个最简洁的观察者抽象类,以及一个主题基类。主题基类提供三个公开方法:attach用于订阅,detach用于取消订阅,notify用于广播。注意这里使用std::weak_ptr来管理观察者,防止主题长期持有导致观察者无法释放。

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

// 观察者抽象接口
class Observer {
public:
    virtual ~Observer() = default;
    virtual void update(const std::string& event) = 0;
};

// 主题基类
class Subject {
public:
    virtual ~Subject() = default;

    void attach(std::weak_ptr<Observer> obs) {
        observers.push_back(obs);
    }

    void detach(std::weak_ptr<Observer> obs) {
        observers.erase(
            std::remove_if(observers.begin(), observers.end(),
                [&](const std::weak_ptr<Observer>& w) {
                    auto s = w.lock();
                    auto o = obs.lock();
                    return !s || !o || s == o;
                }),
            observers.end());
    }

protected:
    void notify(const std::string& event) {
        for (auto it = observers.begin(); it != observers.end(); ) {
            if (auto obs = it->lock()) {
                obs->update(event);
                ++it;
            } else {
                it = observers.erase(it);
            }
        }
    }

private:
    std::vector<std::weak_ptr<Observer>> observers;
};

上面的代码展示了如何用weak_ptr避免循环引用。当某个观察者对象被销毁后,其对应的weak_ptr会失效,在notify时我们顺手清理掉失效的条目。这种方式比裸指针安全,也比shared_ptr更轻量,不会阻止观察者正常析构。

二、具体主题与观察者的实现

有了基类之后,我们可以实现一个具体的温度传感器主题,以及控制台显示器和日志器两个具体观察者。温度传感器在数值变化时调用notify,把所有订阅者都通知一遍。

2.1 具体主题类

具体主题继承Subject,并增加业务方法setTemperature。该方法在内部修改状态后,调用受保护的notify把事件字符串发出去。事件内容可以自定义,这里简单传温度值。

class TemperatureSensor : public Subject {
public:
    void setTemperature(double temp) {
        temperature = temp;
        notify("Temperature changed to " + std::to_string(temp));
    }

private:
    double temperature = 0.0;
};

这种写法把“状态变更”和“通知他人”绑定在同一个业务方法里,但对外部模块而言,它们只需要关心自己是否收到了update调用,完全不用知道主题内部如何存储温度。如果将来要增加湿度传感器,只需再写一个类似的主题类,复用同一套观察者容器机制。

2.2 具体观察者类

下面两个观察者都继承Observer,分别把事件打印到控制台和写入模拟日志。由于它们只依赖抽象接口,彼此之间毫无关联,可以独立测试与部署。

class ConsoleDisplay : public Observer {
public:
    void update(const std::string& event) override {
        std::cout << "[Console] " << event << std::endl;
    }
};

class FileLogger : public Observer {
public:
    void update(const std::string& event) override {
        std::cout << "[Logger] recorded: " << event << std::endl;
    }
};

从代码可以看出,具体观察者没有任何关于主题的成员变量,它们甚至不知道自己被几个主题订阅。这种单向依赖让系统更容易扩展:比如要新增一个短信告警观察者,只需添加新类并在主程序中attach即可,原有类一行都不用改。

三、使用标准库function简化实现

如果项目不想定义那么多接口类,也可以用std::function搭配std::signal风格来做轻量级事件通知。核心思想是把回调函数直接存进容器,主题不再依赖Observer抽象类,进一步降低类型耦合。

3.1 基于function的事件中心

下面实现一个简单的事件分发器,支持任意可调用对象订阅指定事件名。它用map管理事件到处理函数列表的映射,在触发时依次调用。这种写法在小型工具或脚本化逻辑里非常高效。

#include <functional>
#include <unordered_map>
#include <list>

class EventCenter {
public:
    using Handler = std::function<void(const std::string&)>;

    void subscribe(const std::string& name, Handler h) {
        handlers[name].push_back(h);
    }

    void fire(const std::string& name, const std::string& data) {
        if (auto it = handlers.find(name); it != handlers.end()) {
            for (auto& h : it->second) {
                h(data);
            }
        }
    }

private:
    std::unordered_map<std::string, std::list<Handler>> handlers;
};

这种机制牺牲了一定的类型安全(因为Handler签名统一),但换来了极高的灵活性。你可以直接传lambda、成员函数包装后的function,甚至绑定参数的可调用对象。在游戏开发或UI框架里,这类事件中心非常常见。

3.2 两种方案对比

我们用一个表格总结经典接口式与function式的差异,帮助根据实际场景选型。

维度抽象接口观察者function事件中心
耦合程度主题依赖Observer基类主题不依赖具体类型
扩展性新增观察者需写新类直接传lambda即可
类型安全编译期接口约束强运行时签名统一
适用规模中大型长期维护项目快速原型或轻量模块

从维护角度看,接口式更适合边界清晰、观察者数量固定且需要强制实现规范的系统;function式则适合事件种类多、处理逻辑零散的场景。两者都达成了事件通知与解耦的目标,只是抽象层级不同。

四、生命周期与线程安全注意点

在真实C++服务中,观察者和主题可能分属不同线程。如果主题在notify时观察者正在析构,就可能发生竞态。前面用weak_ptr已经解决了部分悬空指针问题,但在多线程下仍需加锁保护容器。

4.1 加锁的线程安全主题

我们可以给Subject的容器操作加上std::mutex,确保attach、detach和notify不会同时修改或遍历同一份数据。以下示例仅展示核心改动。

#include <mutex>

class SafeSubject : public Subject {
protected:
    void notify(const std::string& event) {
        std::lock_guard<std::mutex> lock(mtx);
        // 原notify逻辑,注意lock期间不要调用外部可能回注册的长函数
        Subject::notify(event);
    }
private:
    std::mutex mtx;
};

需要注意的是,如果在observer的update内部又调用了主题的attach或detach,就可能造成死锁或迭代器失效。因此良好的实践是:update里只做轻量响应,重订阅操作放到主题外部的主线程或专用队列里处理。

4.2 避免通知过程中的自我修改

有些开发者喜欢在update里直接detach自己,这会让主题的遍历过程修改容器。推荐做法是让观察者标记“待注销”,主题在notify结束后统一清理,或者改用copy-on-write方式在通知前拷贝一份观察者列表。

设计观察者模式时,永远假设观察者是不可信的:它可能很慢、可能抛异常、可能在回调中修改主题。主题要做的是隔离这些风险,而不是依赖观察者遵守约定。

通过合理的容器选择、智能指针与锁策略,C++里的观察者模式既能保持灵活解耦,也能在复杂环境下稳定运行。无论是传统接口方案还是现代function方案,核心都是把“谁关心变化”和“变化如何发生”彻底分开。

observer_patternC++_eventdecoupling修改时间:2026-08-10 07:03:46

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。