C++观察者模式实战应用插图

C++观察者模式实战应用:从理论到生产级实现

你好,我是源码库的博主。今天想和你深入聊聊C++中的观察者模式。这个模式我几乎在每个大型项目里都用过,从GUI事件处理到游戏引擎的消息系统,再到微服务间的状态同步,它无处不在。但说实话,第一次实现时我也踩了不少坑——内存泄漏、线程安全、循环依赖,该遇到的“惊喜”一个没少。所以这篇文章,我想结合这些实战经验,带你从零构建一个生产可用的观察者模式实现,而不仅仅是教科书上的“Hello World”。

一、为什么我们需要观察者模式?

让我从一个真实场景说起。去年我负责一个实时数据监控系统,需要将同一份市场行情数据同时推送给日志模块、风控模块和UI显示模块。最初我用最直接的方式:在数据接收处硬编码调用这三个模块的接口。结果呢?每加一个新模块(比如后来需要的报警模块),我就得修改核心数据接收代码,单元测试全部重跑,还差点因为一次紧急修改引入了死循环。这种紧耦合的设计,简直就是维护的噩梦。

观察者模式正是为了解决这种一对多的依赖关系而生的。它的核心思想很简单:让主题(Subject)维护一个观察者(Observer)列表,当主题状态变化时,自动通知所有观察者,而主题本身不需要知道观察者的具体细节。这完美符合了开放-封闭原则——对扩展开放,对修改封闭。

二、基础实现:先让它跑起来

我们先从最经典的实现开始。这里我建议你打开编辑器跟着敲一遍,理解会更深刻。

// Observer.hpp - 观察者接口
class Observer {
public:
    virtual ~Observer() = default;
    virtual void update(const std::string& message) = 0;
};

// Subject.hpp - 主题接口
class Subject {
public:
    virtual ~Subject() = default;
    virtual void attach(Observer* observer) = 0;
    virtual void detach(Observer* observer) = 0;
    virtual void notify(const std::string& message) = 0;
};

// ConcreteSubject.hpp - 具体主题
class ConcreteSubject : public Subject {
private:
    std::vector observers_;
    
public:
    void attach(Observer* observer) override {
        observers_.push_back(observer);
    }
    
    void detach(Observer* observer) override {
        observers_.erase(
            std::remove(observers_.begin(), observers_.end(), observer),
            observers_.end()
        );
    }
    
    void notify(const std::string& message) override {
        for (auto* observer : observers_) {
            observer->update(message);
        }
    }
    
    // 业务方法:价格更新
    void setPrice(double price) {
        price_ = price;
        notify("Price updated to: " + std::to_string(price));
    }
    
private:
    double price_;
};

这个基础版本能工作,但在生产环境中直接使用会出问题。我踩过的第一个坑:裸指针管理。如果观察者先于主题被销毁,主题持有的就是野指针,notify时必然崩溃。

三、进阶实现:解决内存与线程安全问题

基于实战教训,我们来构建一个更健壮的版本。这里有几个关键改进点:

// SafeObserver.hpp - 使用智能指针的增强版
#include 
#include 
#include 

class SafeObserver : public std::enable_shared_from_this {
public:
    virtual ~SafeObserver() = default;
    virtual void update(const std::string& message) = 0;
    
    // 提供弱指针供主题持有
    std::weak_ptr getWeakPtr() {
        return weak_from_this();
    }
};

class ThreadSafeSubject {
private:
    std::vector<std::weak_ptr> observers_;
    mutable std::mutex mutex_;  // mutable允许在const方法中加锁
    
public:
    void attach(std::shared_ptr observer) {
        std::lock_guard lock(mutex_);
        observers_.push_back(observer->getWeakPtr());
    }
    
    void detach(std::shared_ptr observer) {
        std::lock_guard lock(mutex_);
        auto it = std::remove_if(observers_.begin(), observers_.end(),
            [&](const std::weak_ptr& wp) {
                auto sp = wp.lock();
                return !sp || sp == observer;
            });
        observers_.erase(it, observers_.end());
    }
    
    void notify(const std::string& message) {
        std::vector<std::shared_ptr> validObservers;
        
        {
            std::lock_guard lock(mutex_);
            // 清理过期观察者并收集有效观察者
            auto it = std::remove_if(observers_.begin(), observers_.end(),
                [&](std::weak_ptr& wp) {
                    return wp.expired();
                });
            observers_.erase(it, observers_.end());
            
            // 锁定弱指针
            for (auto& wp : observers_) {
                if (auto sp = wp.lock()) {
                    validObservers.push_back(sp);
                }
            }
        }
        
        // 在锁外调用update,避免死锁和性能问题
        for (auto& observer : validObservers) {
            observer->update(message);
        }
    }
};

这个版本解决了几个关键问题:

  1. 使用weak_ptr避免循环引用:主题持有观察者的弱引用,不影响其生命周期
  2. 自动清理过期观察者:在notify时自动移除已被销毁的观察者
  3. 线程安全:使用互斥锁保护观察者列表,但注意update调用在锁外执行

四、实战技巧:模板化与性能优化

在实际项目中,我们经常需要传递不同类型的数据。硬编码消息类型会限制扩展性。下面是我在交易系统中使用的模板化方案:

// 事件基类
struct Event {
    virtual ~Event() = default;
    virtual std::string type() const = 0;
};

// 模板化观察者
template
class TypedObserver {
public:
    virtual ~TypedObserver() = default;
    virtual void onEvent(const EventType& event) = 0;
};

// 类型安全的主题
class EventBus {
private:
    // 类型擦除存储
    class ObserverWrapper {
    public:
        virtual ~ObserverWrapper() = default;
        virtual void notify(const Event& event) = 0;
    };
    
    template
    class TypedObserverWrapper : public ObserverWrapper {
        TypedObserver* observer_;
        
    public:
        TypedObserverWrapper(TypedObserver* observer)
            : observer_(observer) {}
            
        void notify(const Event& event) override {
            if (event.type() == EventType::staticType()) {
                observer_->onEvent(static_cast(event));
            }
        }
    };
    
    std::unordered_map<std::string, std::vector<std::unique_ptr>> observers_;
    std::mutex mutex_;
    
public:
    template
    void subscribe(TypedObserver* observer) {
        std::lock_guard lock(mutex_);
        observers_[EventType::staticType()].push_back(
            std::make_unique<TypedObserverWrapper>(observer)
        );
    }
    
    void publish(const Event& event) {
        std::vector toNotify;
        
        {
            std::lock_guard lock(mutex_);
            auto it = observers_.find(event.type());
            if (it != observers_.end()) {
                for (auto& wrapper : it->second) {
                    toNotify.push_back(wrapper.get());
                }
            }
        }
        
        for (auto* wrapper : toNotify) {
            wrapper->notify(event);
        }
    }
};

这个设计的好处是类型安全且高效。每个事件类型有独立的观察者列表,发布时只需查找对应类型,避免了遍历所有观察者。

五、避坑指南:我踩过的那些坑

1. 在update中修改观察者列表:曾经有观察者在update方法中detach自己,导致迭代器失效崩溃。解决方案:像上面那样,先复制列表再通知。

2. update抛出异常:一个观察者的异常会影响其他观察者。建议:

try {
    observer->update(message);
} catch (...) {
    // 记录日志,但继续通知其他观察者
    logger.error("Observer update failed");
}

3. 性能瓶颈:高频事件(如鼠标移动)可能产生大量通知。解决方案:批量处理或使用异步通知队列。

4. 循环通知:A通知B,B又通知A,形成死循环。一定要在设计时理清依赖关系。

六、现代C++的优雅实现(C++17+)

如果你能用C++17或更高版本,可以写出更简洁的代码:

template
class Signal {
    using Slot = std::function;
    std::vector<std::weak_ptr> slots_;
    
public:
    auto connect() -> std::shared_ptr {
        auto slot = std::make_shared();
        slots_.emplace_back(slot);
        return slot;
    }
    
    void emit(Args... args) {
        // 使用结构化绑定和if初始化
        for (auto it = slots_.begin(); it != slots_.end(); ) {
            if (auto slot = it->lock()) {
                (*slot)(args...);
                ++it;
            } else {
                it = slots_.erase(it);
            }
        }
    }
};

// 使用示例
Signal dataSignal;
auto connection = dataSignal.connect();
*connection = [](int id, const std::string& name) {
    std::cout << "Received: " << id << ", " << name << std::endl;
};
dataSignal.emit(42, "Answer");

七、实际应用场景

在我的项目中,观察者模式主要用在:

  1. 配置热更新:配置文件修改后,自动通知所有使用配置的模块
  2. 游戏引擎:实体组件系统(ECS)中的事件系统
  3. 交易系统:行情变化时通知策略引擎、风控、日志等模块
  4. UI框架:按钮点击、数据变化等事件处理

观察者模式是解耦的利器,但也要避免滥用。如果观察者链过长或关系复杂,考虑使用更专门的消息总线(Message Bus)或响应式编程库。

最后给个建议:在项目初期就设计好事件/消息系统的基础设施,后期重构成本会很高。希望我的这些经验能帮你少走弯路。如果有具体问题,欢迎在源码库社区交流讨论!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。