【Head First 设计模式】第 2 章:观察者模式 —— 用现代 C++ 重新解读“交互对象的松耦合

【Head First 设计模式】第 2 章:观察者模式 ------ 用现代 C++ 重新解读"交互对象的松耦合

导语 :电台广播不会跑到你家去确认你有没有在听,也不会替你决定你什么时候该关掉收音机。可如果换成 C++ 里的"广播者"(Subject)持有一堆指向"听众"(Observer)的裸指针,事情就没那么潮洒了------听众突然消失,广播者却毫不知情,继续对着一个已经不存在的对象喊话。本文用气象站案例讲清楚观察者模式,更用 std::weak_ptr 解决"广播者该不该管听众生死"这个 C++ 特有的问题。

写在前面:从 Java 切换到 C++ 视角后,核心改变点是什么

《Head First 设计模式》用 Java 讲观察者模式时,重点是"一对多的通知关系"和"解耦数据源与展示方式"。这部分思想不受语言影响,换到 C++ 依然成立。

但 Java 版本有一个隐藏假设,很容易被忽略:有垃圾回收器兜底 。观察者忘记调用 removeObserver(),最多是内存泄漏(对象因为还被引用而不会被回收),程序不会立刻崩溃。

C++ 没有这层安全网。如果 Subject 内部存的是指向 Observer 的裸指针或者强引用,而某个 Observer 先于 Subject 销毁却没有注销,notify() 执行到它时就是未定义行为------轻则输出乱码,重则直接崩溃。

反过来,如果 Observer 为了主动"拉"数据也持有 Subject,两者互相持有对方,还会遇到循环引用的问题。

所以这一章要解决两层问题:

  1. 用观察者模式把"数据变化"和"如何展示"解耦,理解一对多通知关系。
  2. 用现代 C++ 讲清楚:广播者和听众之间,到底谁该为谁的生命周期负责。

先从一个正在报错边缘的气象站开始。


一、气象站问题:紧耦合的报表系统为什么难扩展

《Head First 设计模式》里的经典场景:气象站测得温度、湿度、气压后,需要同时更新多个报表------当前状况、统计信息、天气预报。

1.1 最直觉的写法:谁都认识谁

刚接触这个需求时,很容易写出这样的代码:

cpp 复制代码
#include <iostream>

class CurrentConditionsDisplay
{
public:
    void update(float temperature, float humidity, float pressure)
    {
        std::cout << "Current conditions: " << temperature
                   << "F degrees and " << humidity << "% humidity\n";
    }
};

class StatisticsDisplay
{
public:
    void update(float temperature, float humidity, float pressure)
    {
        std::cout << "Avg/Max/Min stats updated.\n";
    }
};

class ForecastDisplay
{
public:
    void update(float temperature, float humidity, float pressure)
    {
        std::cout << "Forecast: more of the same.\n";
    }
};

class WeatherData
{
public:
    void measurementsChanged()
    {
        float temperature = getTemperature();
        float humidity = getHumidity();
        float pressure = getPressure();

        currentDisplay_.update(temperature, humidity, pressure);
        statisticsDisplay_.update(temperature, humidity, pressure);
        forecastDisplay_.update(temperature, humidity, pressure);
    }

    void setMeasurements(float temperature, float humidity, float pressure)
    {
        temperature_ = temperature;
        humidity_ = humidity;
        pressure_ = pressure;
        measurementsChanged();
    }

private:
    float getTemperature() const { return temperature_; }
    float getHumidity() const { return humidity_; }
    float getPressure() const { return pressure_; }

    float temperature_ = 0.0f;
    float humidity_ = 0.0f;
    float pressure_ = 0.0f;

    CurrentConditionsDisplay currentDisplay_;
    StatisticsDisplay statisticsDisplay_;
    ForecastDisplay forecastDisplay_;
};

这段代码能跑,输出也符合预期。问题不在语法,而在依赖关系:WeatherData 的头文件里,必须 #include 每一种具体 Display 的头文件。

1.2 暴露的问题

假设现在要新增一个第三方报表 ThirdPartyDisplay,步骤会是:

  1. 打开 WeatherData 的源码。
  2. 在成员变量里新增一个 thirdPartyDisplay_。
  3. 在 measurementsChanged() 里新增一行 thirdPartyDisplay_.update(...)。
  4. 重新编译 WeatherData------即便它的核心逻辑(测量数据)根本没有变化。

WeatherData 本该只负责"管理气象数据",现在却被迫认识每一个具体的报表类型,还要在数据变化时手动挨个通知。新增一种报表,代价是修改一个本不该被修改的类。

1.3 定位真正变化的部分

和第1章鸭子案例的分析方法一样,先问一句:这里什么是稳定的,什么是易变的?

  • 相对稳定的:气象站测量数据的逻辑(getTemperature() 等)、数据变化后"需要通知关心的对象"这件事本身。
  • 容易变化的:谁在关心数据变化------报表的种类会增加、减少,甚至运行时动态开关。

WeatherData 不应该硬编码"通知哪几个具体对象",而应该只负责"数据变了,通知所有关心的人",不关心"关心的人"具体是谁。这正是观察者模式要解决的问题。


二、先建立直觉:广播 vs 主动去问

在看代码前,先把"被观察者"和"观察者"这两个角色说清楚。

2.1 被观察者(Subject):像电台广播

电台主持人开播时,不知道也不关心具体是谁在收听。他只管把信号发出去,收音机开着的人自然会听到。这就是 Subject 的角色:

我的状态变化了,我会发出通知。谁在听,我不关心;听众自己决定什么时候订阅、什么时候退订。

WeatherData 就应该是这样的广播者:它不认识 CurrentConditionsDisplay,只认识"观察者"这个抽象身份。

2.2 观察者(Observer):像订阅了电台的听众

反过来,Observer 的角色是主动订阅:

我对你的状态变化感兴趣,请把我加入你的通知列表。我随时可以退订,你不该替我决定我什么时候不想再听了。

这里有个容易被忽略但很关键的比喻延伸:广播站不会因为一个听众家里断电(对象销毁)就自动清空他的订阅记录 ------现实中的电台无所谓,但 C++ 里的 Subject 如果不知道听众已经"断电",继续对着空气喊话,就是程序层面的未定义行为。这也是本章第四节要处理的核心问题。

2.3 推模型 vs 拉模型

通知的方式有两种:

  • 推模型(Push) :Subject 在 notify() 时,直接把最新的温度、湿度、气压当作参数传给每个 Observer::update(...)。
  • 拉模型(Pull) :Subject 只发一个"数据变了"的信号,Observer 收到信号后自己反过来调用 Subject 的 getter,主动去"拉"最新状态。

气象站案例用推模型更自然,因为每次通知都需要三个数据,参数不多不少刚好合适。拉模型更适合观察者只需要其中一部分数据、或者数据种类经常变化的场景。本章以推模型为主线,第四节会提到拉模型带来的所有权问题。

2.4 解耦的本质

把这两个角色的关系画出来:

text 复制代码
重构前:WeatherData 直接认识每一种具体 Display
重构后:WeatherData 只认识 Observer 接口
        CurrentConditionsDisplay / StatisticsDisplay / ForecastDisplay
        都只是"实现了 Observer 接口的某个类"

WeatherData 的代码里,从此不会出现任何一个具体 Display 类的名字。这正是"面向接口编程,而不是面向实现编程"在观察者模式里的体现。


三、观察者模式:定义一对多的通知关系

先用一句朴素的话理解观察者模式:

定义对象之间的一对多依赖关系,当一个对象的状态改变时,所有依赖它的对象都会自动收到通知并更新。

3.1 定义 Observer 与 Subject 接口

cpp 复制代码
// 观察者接口
class Observer
{
public:
    virtual ~Observer() = default;
    virtual void update(float temperature, float humidity, float pressure) = 0;
};
// 被观察者(Subject)接口
class Subject
{
public:
    virtual ~Subject() = default;
    virtual void registerObserver(std::weak_ptr<Observer> observer) = 0;
    virtual void removeObserver(const std::shared_ptr<Observer>& observer) = 0;
    virtual void notifyObservers() = 0;
};

和第1章一样,两个接口都需要虚析构函数。这里先直接用 std::weak_ptr<Observer> 作为 registerObserver 的参数类型------原因会在第四节详细展开,本节先把基本结构搭起来。

3.2 WeatherData 实现 Subject

cpp 复制代码
#include <algorithm>
#include <memory>
#include <vector>

class WeatherData final : public Subject
{
public:
    void registerObserver(std::weak_ptr<Observer> observer) override
    {
        observers_.push_back(std::move(observer));
    }
    
	// 移除指定的观察者,同时自动清理已经失效的弱引用
    void removeObserver(const std::shared_ptr<Observer>& observer) override
    {
        // 采用 C++ 标准库经典的"Erase-remove 范式"(Erase-remove idiom)来高效删除元素。
        // std::remove_if 负责把"需要保留的元素"移到 vector 前面,返回一个分隔两区的迭代器;
        // observers_.erase 负责把后面那段"不需要的元素"在物理内存中真正抹去。
        observers_.erase(
            std::remove_if(observers_.begin(), observers_.end(),
                // Lambda 表达式(匿名闭包函数):作为谓词传递给 remove_if。
                // [&observer] 表示按引用捕获外部传入的待删除指针 observer,避免指针拷贝。
                [&observer](const std::weak_ptr<Observer>& weak)
                {
                    // 1. 尝试将 weak_ptr 提升为强引用 shared_ptr
                    //    因为 weak_ptr 本身无法直接获取指向的对象或进行比较,必须调用 lock()。
                    //    如果原对象还活着,locked 会是一个指向该对象的 shared_ptr;
                    //    如果原对象已经被析构(在外部被销毁),locked 会是一个空的 shared_ptr (nullptr)。
                    auto locked = weak.lock();

                    // 2. 返回布尔值告诉 remove_if 这个元素要不要被删掉 (true = 删除, false = 保留):
                    //    条件一:!locked
                    //            说明对象在外部已经被销毁了(悬空引用)。既然对象都没了,
                    //            留在 observers_ 列表里也是浪费空间,顺便把它清理掉(懒清理机制)。
                    //    条件二:locked == observer
                    //            说明提升后的 shared_ptr 和我们要寻找并注销的观察者地址相同(指向同一个内存对象)。
                    //    只要满足其中任意一个条件(用 || 连接),该元素就会被remove_if标记为删除。
                    return !locked || locked == observer;
                }),
            observers_.end());
    }

    void notifyObservers() override
    {
        for (auto& weak : observers_)
        {
            // C++ 条件初始化语法糖:
            // 1. 调用 weak.lock() 尝试将弱引用提升为 shared_ptr
            // 2. 在 if 内部定义临时变量 observer
            // 3. 如果对象还没被销毁,observer 非空(条件为 true),进入分支
            // 4. observer 强引用会在 if 作用域内保持对象存活,防止执行 update 时对象突遭销毁
            if (auto observer = weak.lock())
            {
                observer->update(temperature_, humidity_, pressure_);
            }
        }
    }

    void setMeasurements(float temperature, float humidity, float pressure)
    {
        temperature_ = temperature;
        humidity_ = humidity;
        pressure_ = pressure;
        measurementsChanged();
    }

private:
    void measurementsChanged()
    {
        notifyObservers();
    }

    float temperature_ = 0.0f;
    float humidity_ = 0.0f;
    float pressure_ = 0.0f;

    std::vector<std::weak_ptr<Observer>> observers_;
};

留意这几处设计:

  1. observers_ 存的是 std::weak_ptr<Observer>,不是强引用------WeatherData 不应该决定观察者的生死,这一点会在第四节详细展开。
  2. notifyObservers() 里用 weak.lock() 判断观察者是否还活着,活着才通知,已经销毁的直接跳过。
  3. removeObserver() 顺手清理掉已经失效(lock() 返回空)的条目,避免列表越攒越大。
  4. measurementsChanged() 保留为私有方法,对外只暴露 setMeasurements()------这和书中原始设计一致,调用方不需要关心"数据变化"和"通知"是分两步走的。

3.3 具体 Display 实现 Observer

cpp 复制代码
#include <iostream>
//std::enable_shared_from_this<T> 是 C++11 引入的一个模板类,主要用来解决一个关键问题:在一个被 std::shared_ptr 管理的对象内部,安全地获取指向自身的 std::shared_ptr
class CurrentConditionsDisplay final : public Observer,
                                        public std::enable_shared_from_this<CurrentConditionsDisplay>
{
public:
    void update(float temperature, float humidity, float pressure) override
    {
        std::cout << "Current conditions: " << temperature
                   << "F degrees and " << humidity << "% humidity\n";
    }
};

class StatisticsDisplay final : public Observer,
                                 public std::enable_shared_from_this<StatisticsDisplay>
{
public:
    void update(float temperature, float humidity, float pressure) override
    {
        maxTemp_ = std::max(maxTemp_, temperature);
        minTemp_ = std::min(minTemp_, temperature);
        sumTemp_ += temperature;
        ++count_;

        std::cout << "Avg/Max/Min temperature: "
                   << sumTemp_ / count_ << "/" << maxTemp_ << "/" << minTemp_ << '\n';
    }

private:
    float maxTemp_ = std::numeric_limits<float>::lowest();
    float minTemp_ = std::numeric_limits<float>::max();
    float sumTemp_ = 0.0f;
    int count_ = 0;
};

class ForecastDisplay final : public Observer,
                               public std::enable_shared_from_this<ForecastDisplay>
{
public:
    void update(float temperature, float humidity, float pressure) override
    {
        std::cout << "Forecast: more of the same.\n";
    }
};

三个 Display 都继承了 std::enable_shared_from_this。原因很直接:如果这个类的实例将来要把自己注册成观察者(registerObserver(shared_from_this())),就需要一种安全的方式从"我自己"生成一个 std::weak_ptr------直接构造 std::shared_ptr<CurrentConditionsDisplay>(this) 会导致引用计数被重复管理,最终重复释放。这一点第四节会展开讲。

3.4 组装并运行

cpp 复制代码
int main()
{
    WeatherData weatherData;

    auto currentDisplay = std::make_shared<CurrentConditionsDisplay>();
    auto statisticsDisplay = std::make_shared<StatisticsDisplay>();
    auto forecastDisplay = std::make_shared<ForecastDisplay>();

    weatherData.registerObserver(currentDisplay);
    weatherData.registerObserver(statisticsDisplay);
    weatherData.registerObserver(forecastDisplay);

    weatherData.setMeasurements(80, 65, 30.4f);
    weatherData.setMeasurements(82, 70, 29.2f);

    weatherData.removeObserver(forecastDisplay);
    weatherData.setMeasurements(78, 90, 29.2f);
}

输出:

text 复制代码
Current conditions: 80F degrees and 65% humidity
Avg/Max/Min temperature: 80/80/80
Forecast: more of the same.
Current conditions: 82F degrees and 70% humidity
Avg/Max/Min temperature: 81/82/80
Forecast: more of the same.
Current conditions: 78F degrees and 90% humidity
Avg/Max/Min temperature: 80/82/78

第三次 setMeasurements() 之后,ForecastDisplay 已经被 removeObserver() 移除,所以它不再收到通知,输出里也就没有再出现"Forecast: more of the same."。

registerObserver 接收 std::weak_ptr<Observer>,但传进去的是 std::shared_ptr------这是合法的隐式转换,shared_ptr 可以直接转成对应的 weak_ptr。weatherData 本身不持有任何一个 Display 的强引用,三个 shared_ptr 的生命周期完全由 main() 的局部变量控制。


四、C++ 核心解构:悬空指针与生命周期地雷

上一节的代码能正确运行,但那是因为三个 shared_ptr 一直活到 main() 结束。如果情况反过来呢?

4.1 最大风险场景:Observer 先于 Subject 销毁

假设 WeatherData 内部存的不是 weak_ptr,而是裸指针:

cpp 复制代码
std::vector<Observer*> observers_;  // 危险写法,仅用于说明问题

再看这样一段调用:

cpp 复制代码
void run(WeatherData& weatherData)
{
    CurrentConditionsDisplay display;
    weatherData.registerObserver(&display);
}   // display 在这里被销毁

int main()
{
    WeatherData weatherData;
    run(weatherData);
    weatherData.setMeasurements(80, 65, 30.4f);  // 未定义行为
}

display 是 run() 里的局部对象,函数返回时就销毁了。但 weatherData.observers_ 里仍然存着指向它的裸指针。等到 setMeasurements() 触发 notifyObservers(),代码会对着一块已经释放的内存调用虚函数------这就是悬空指针(dangling pointer),结果可能是崩溃,也可能是更隐蔽的数据错乱。

这正是本章开头提到的"Java 的隐藏假设":Java 里 display 只要还被 observers 列表引用,就不会被回收;C++ 里对象的生命周期由作用域决定,和"是否还被引用"完全无关。

4.2 用 std::weak_ptr 表达"广播者不该决定听众的生死"

第三节的代码已经给出了答案:WeatherData 内部存的是 std::vector<std::weak_ptr<Observer>>,不是裸指针,也不是 std::shared_ptr。这个选择背后的逻辑是:

观察者的生命周期应该由持有它的那一方(通常是 main() 或者更上层的对象)决定,WeatherData 只是"知道"这些观察者的存在,不应该、也不需要延长它们的生命周期。

如果把 registerObserver 的参数类型换成 std::shared_ptr<Observer> 并直接存起来,WeatherData 就会成为观察者的共同持有者之一。这看起来"更安全",实际上引入了一个新问题:观察者的生命周期会被 WeatherData 不知不觉地延长,即使调用方已经不再持有它、以为它该销毁了,它依然活着,因为 WeatherData 还在。

weak_ptr 不参与引用计数,不会影响对象的生死,只负责"在需要的时候确认它还活着"。这正是 4.1 节问题的正确解法:

cpp 复制代码
void notifyObservers() override
{
    for (auto& weak : observers_)
    {
        if (auto observer = weak.lock())   // 判活
        {
            observer->update(temperature_, humidity_, pressure_);
        }
        // lock() 返回空,说明观察者已经销毁,直接跳过,不会崩溃
    }
}

回到 4.1 节的例子,如果 observers_ 存的是 weak_ptr,display 销毁后,对应的 weak_ptr::lock() 会返回空指针,notifyObservers() 只是安静地跳过它,不会有任何未定义行为。

4.3 循环引用陷阱:拉模型下的反向持有

2.3 节提到过拉模型------观察者收到通知后,反过来调用 Subject 的接口去"拉"数据。这要求 Observer 也持有一份指向 Subject 的引用:

cpp 复制代码
class PullBasedDisplay final : public Observer
{
public:
    explicit PullBasedDisplay(std::shared_ptr<WeatherData> subject)
        : subject_(std::move(subject))   // 危险:双向 shared_ptr
    {
    }

    void update(float, float, float) override
    {
        std::cout << subject_->getTemperature() << '\n';
    }

private:
    std::shared_ptr<WeatherData> subject_;
};

如果 WeatherData 那边又用 shared_ptr 强引用这个 PullBasedDisplay,就形成了经典的循环引用:WeatherData 持有 Display,Display 持有 WeatherData,两者的引用计数永远不会归零,谁都不会被析构,内存泄漏且不会有任何报错提示你。

解法很直接:两个方向里,至少有一个必须是弱引用 。观察者模式里几乎总是"Subject 弱引用 Observer"这个方向更合理(呼应 4.2 节的结论),如果 Observer 也要反向持有 Subject,应该用裸指针或 std::weak_ptr<WeatherData>,而不是 shared_ptr:

cpp 复制代码
private:
    std::weak_ptr<WeatherData> subject_;   // 安全:不参与引用计数

4.4 RAII 自动注销:不必依赖手动调用 removeObserver

第三节的 main() 里手动调用了一次 removeObserver(forecastDisplay)。如果忘记调这一行会怎样?由于 observers_ 存的是 weak_ptr,forecastDisplay 销毁后 notifyObservers() 依然会安全跳过它,不会崩溃------但列表里会一直留着一个"已失效"的条目,直到下一次 removeObserver() 顺手清理,或者本节末尾的技巧主动清理。

如果希望观察者析构时自动完成注销,可以引入一层轻量的包装,在析构函数里调用 removeObserver:

cpp 复制代码
class ScopedObserver
{
public:
    ScopedObserver(std::shared_ptr<Subject> subject,
                   std::shared_ptr<Observer> observer)
        : subject_(std::move(subject)), observer_(std::move(observer))
    {
        subject_->registerObserver(observer_);
    }

    ~ScopedObserver()
    {
        subject_->removeObserver(observer_);
    }

private:
    std::shared_ptr<Subject> subject_;
    std::shared_ptr<Observer> observer_;
};

这是 RAII(Resource Acquisition Is Initialization)思想在观察者模式里的应用:把"注册"和"注销"这一对操作绑定到一个对象的构造和析构上,调用方不需要记得手动配对调用。对于生命周期明确、订阅关系和某个作用域强绑定的场景,这比裸调用 registerObserver/removeObserver 更不容易出错。

4.5 小结:三条生命周期规则

把本节的结论收拢成三条规则:

  1. Subject 存观察者时,优先用 weak_ptr,不要用裸指针(会悬空),也不要不假思索用 shared_ptr`(会不必要地延长生命周期)。
  2. 拉模型下 Observer 反向持有 Subject 时,同样避免双向 shared_ptr,至少一个方向要弱引用。
  3. 如果订阅关系和某个作用域强绑定,用 RAII 包装注册/注销这一对操作,而不是依赖"记得手动调用"。

五、Modern C++ 简化版:从接口类到 Signal/Slot

三个 Display 目前都只做一件事:接收三个 float,打印一行文字。为了这么简单的逻辑,专门定义一个 Observer 接口、再各写一个实现类,未免有点"仪式感过重"。C++11 之后,std::function 和 Lambda 给了另一种落地方式。

同样先澄清:这不是 Java 做不到的事,现代 Java 也有函数式接口。这里讨论的是 C++ 里可调用对象带来的另一种写法。

5.1 手写一个极简 Signal

cpp 复制代码
#include <functional>
#include <vector>

template <typename... Args>
class Signal
{
public:
    using Slot = std::function<void(Args...)>;

    void connect(Slot slot)
    {
        slots_.push_back(std::move(slot));
    }

    void emit(Args... args) const
    {
        for (const auto& slot : slots_)
        {
            slot(args...);
        }
    }

private:
    std::vector<Slot> slots_;
};

Signal<float, float, float> 就相当于第三节里 Subject 接口 + notifyObservers() 的合体:connect() 对应 registerObserver(),emit() 对应 notifyObservers()。

5.2 用 Signal 重写 WeatherData

cpp 复制代码
class FunctionalWeatherData
{
public:
    Signal<float, float, float> measurementsChanged;

    void setMeasurements(float temperature, float humidity, float pressure)
    {
        temperature_ = temperature;
        humidity_ = humidity;
        pressure_ = pressure;
        measurementsChanged.emit(temperature_, humidity_, pressure_);
    }

private:
    float temperature_ = 0.0f;
    float humidity_ = 0.0f;
    float pressure_ = 0.0f;
};

订阅时直接用 Lambda,不需要专门定义 CurrentConditionsDisplay 类:

cpp 复制代码
int main()
{
    FunctionalWeatherData weatherData;

    weatherData.measurementsChanged.connect(
        [](float temperature, float humidity, float pressure)
        {
            std::cout << "Current conditions: " << temperature
                       << "F degrees and " << humidity << "% humidity\n";
        });

    float maxTemp = std::numeric_limits<float>::lowest();
    weatherData.measurementsChanged.connect(
        [maxTemp](float temperature, float, float) mutable
        {
            maxTemp = std::max(maxTemp, temperature);
            std::cout << "Max temperature so far: " << maxTemp << '\n';
        });

    weatherData.setMeasurements(80, 65, 30.4f);
    weatherData.setMeasurements(82, 70, 29.2f);
}

没有 Observer 接口,没有具体 Display 类,connect() 接收的就是一段"数据变化时该做什么"的逻辑。

5.3 接口类版 vs Signal/Lambda 版怎么选

场景 更适合接口类版 更适合 Signal/Lambda 版
观察者逻辑只有几行 ✓
观察者需要维护复杂内部状态(如 StatisticsDisplay 的累计值) ✓
需要在观察者析构时自动注销(配合 4.4 节 RAII) ✓
临时订阅一次性逻辑,或写测试时快速插入断言 ✓
希望观察者有清晰的类型名称、可被其他模块引用 ✓

StatisticsDisplay 这种带累计状态、名字本身就说明意图的观察者,写成接口类实现更清楚;main() 里临时订阅一行打印逻辑,用 Lambda 更轻便。

5.4 Lambda 版本依然要面对生命周期问题

Signal 换掉了接口类,但没有换掉第四节讨论的所有权问题。如果 Lambda 按引用捕获了一个即将销毁的对象(或者捕获了 this,而持有该 Lambda 的对象析构得比 Signal 早),一样会留下悬空引用------这一点第1章 5.3 节已经详细讨论过,此处不再重复展开,结论同样适用:优先按值捕获,捕获 this 时确认生命周期关系。


六、学以致用:从气象站迁移到传感器订阅

设计模式真正的价值,不是记住气象站,而是能认出其他问题里的相同结构。

现在有一个场景:一块温度传感器,多个订阅者关心它的读数------日志记录器负责落盘,超温告警器负责报警,显示屏负责实时展示。

映射关系很直接:

气象站系统 传感器系统
WeatherData TemperatureSensor
Observer Observer(沿用同一个接口)
CurrentConditionsDisplay SensorLogger
StatisticsDisplay OverheatAlarm

6.1 复用 Observer 接口,新增两个订阅者

传感器场景只关心一个数值,接口比气象站还简单:

cpp 复制代码
class Observer
{
public:
    virtual ~Observer() = default;
    virtual void update(float value) = 0;
};

class TemperatureSensor final : public Subject
{
public:
    void registerObserver(std::weak_ptr<Observer> observer) override
    {
        observers_.push_back(std::move(observer));
    }

    void removeObserver(const std::shared_ptr<Observer>& observer) override
    {
        observers_.erase(
            std::remove_if(observers_.begin(), observers_.end(),
                [&observer](const std::weak_ptr<Observer>& weak)
                {
                    auto locked = weak.lock();
                    return !locked || locked == observer;
                }),
            observers_.end());
    }

    void notifyObservers() override
    {
        for (auto& weak : observers_)
        {
            if (auto observer = weak.lock())
            {
                observer->update(reading_);
            }
        }
    }

    void setReading(float reading)
    {
        reading_ = reading;
        notifyObservers();
    }

private:
    float reading_ = 0.0f;
    std::vector<std::weak_ptr<Observer>> observers_;
};
cpp 复制代码
class SensorLogger final : public Observer
{
public:
    void update(float value) override
    {
        std::cout << "[log] reading = " << value << '\n';
    }
};

class OverheatAlarm final : public Observer
{
public:
    void update(float value) override
    {
        if (value > threshold_)
        {
            std::cout << "[alarm] overheat! reading = " << value << '\n';
        }
    }

private:
    float threshold_ = 75.0f;
};
cpp 复制代码
int main()
{
    TemperatureSensor sensor;

    auto logger = std::make_shared<SensorLogger>();
    auto alarm = std::make_shared<OverheatAlarm>();

    sensor.registerObserver(logger);
    sensor.registerObserver(alarm);

    sensor.setReading(60.0f);
    sensor.setReading(80.0f);
}

输出:

text 复制代码
[log] reading = 60
[log] reading = 80
[alarm] overheat! reading = 80

结构和气象站案例完全一致:TemperatureSensor 不认识 SensorLogger 或 OverheatAlarm 的具体类型,只认识 Observer 接口;observers_ 依然是 std::weak_ptr,不会替订阅者的生死做决定。新增一种订阅者(比如把读数上报到云端),只需要新写一个 Observer 实现,不用改动 TemperatureSensor。

6.2 运行时增删订阅者:显示屏只在"值班"时监听

工程场景里,订阅关系往往不是固定不变的。比如一块巡检显示屏,只在巡检员打开它的这段时间需要关注读数,巡检结束就该退订:

cpp 复制代码
class InspectionDisplay final : public Observer
{
public:
    void update(float value) override
    {
        std::cout << "[display] current reading = " << value << '\n';
    }
};

void runInspection(TemperatureSensor& sensor)
{
    auto display = std::make_shared<InspectionDisplay>();
    sensor.registerObserver(display);

    sensor.setReading(65.0f);   // 巡检期间正常收到通知

    sensor.removeObserver(display);
}   // display 在这里销毁,但已经提前退订,不会留下悬空的 weak_ptr

int main()
{
    TemperatureSensor sensor;

    runInspection(sensor);

    sensor.setReading(90.0f);   // 巡检已结束,没有任何输出
}

runInspection() 返回后 display 立刻销毁,但因为提前调用了 removeObserver(),observers_ 里不会留下失效条目------这是本章第四节"RAII 自动注销"思路的一个简化版本:订阅的生命周期和"巡检"这个作用域强绑定,进入巡检就注册,巡检结束就注销。

6.3 复现一次生命周期陷阱:忘记退订会怎样

刻意去掉 removeObserver() 这一行,看看第四节讨论的问题在这个场景里如何体现:

cpp 复制代码
void runInspectionWithoutCleanup(TemperatureSensor& sensor)
{
    auto display = std::make_shared<InspectionDisplay>();
    sensor.registerObserver(display);
    sensor.setReading(65.0f);
}   // 忘记调用 removeObserver,display 在这里销毁

int main()
{
    TemperatureSensor sensor;

    runInspectionWithoutCleanup(sensor);

    sensor.setReading(90.0f);   // display 已经不存在
}

因为 observers_ 存的是 std::weak_ptr,不是裸指针,这里不会崩溃------weak.lock() 会返回空指针,notifyObservers() 安静地跳过它。这正是 4.2 节的结论在传感器场景里的直接验证:同样是"忘记清理"的疏忽,裸指针版本会导致未定义行为,weak_ptr 版本只是留下一条不会被触发的失效记录。

如果这个失效记录需要被及时清理(比如订阅者数量很大、长期运行的服务),可以参考 3.2 节 removeObserver() 里 std::remove_if 的写法,定期或在下一次操作时清扫一遍列表。


七、回头看:观察者模式解决了什么

观察者模式不是为了炫技式地引入接口和智能指针,它主要解决的是:一个对象的状态变化,需要让若干个数量不固定、类型可能各不相同的对象知道,而这些对象又不该和状态源死死绑在一起。

重构前,WeatherData 的代码里直接写死了三个具体 Display:

text 复制代码
WeatherData
├── 认识 CurrentConditionsDisplay
├── 认识 StatisticsDisplay
└── 认识 ForecastDisplay

重构后,WeatherData 只认识一个抽象接口:

text 复制代码
WeatherData ──依赖──> Observer 接口
                        ├── CurrentConditionsDisplay
                        ├── StatisticsDisplay
                        └── ForecastDisplay(可随时增删)

新增一种报表,不需要改动 WeatherData;某个报表不再需要,也不需要改动 WeatherData。这两个方向的变化被解耦了。

7.1 这也体现了开闭原则

新增 ThirdPartyDisplay 时,只需要新写一个实现 Observer 的类,不需要修改 WeatherData::notifyObservers()。这正是:

对扩展开放,对修改关闭。

策略模式让"一个对象怎么做某件事"可以独立替换;观察者模式让"一件事发生后该通知谁"可以独立增删。两者都是开闭原则在不同维度上的体现。

7.2 什么时候不必用观察者模式

如果订阅关系是固定的、订阅者数量从一开始就不会变、也没有运行时增删的需求,直接在数据变化处调用几个具体函数,往往比引入接口、weak_ptr 和通知列表更直接。

可以在出现以下信号时考虑观察者模式:

  • 一个对象状态变化后,需要通知的对象数量不固定,或者会随时间增减。
  • 状态源不应该知道、也不应该依赖具体是哪些对象在关心它的变化。
  • 希望在不修改状态源代码的前提下,新增或移除某类响应逻辑。
  • 需要在测试中注入一个"假的"观察者来验证状态变化是否被正确广播。

八、本章 C++ 避坑卡片

卡片 1:广播者存观察者时,优先 weak_ptr,而不是 shared_ptr 或裸指针

cpp 复制代码
std::vector<std::weak_ptr<Observer>> observers_;

裸指针会在观察者先销毁时变成悬空指针,导致未定义行为;shared_ptr 会让广播者无意中延长观察者的生命周期。weak_ptr 准确表达了"广播者不该决定听众的生死"。

卡片 2:通知前用 lock() 判活,不要假设列表里的对象一定还存在

cpp 复制代码
if (auto observer = weak.lock())
{
    observer->update(...);
}

weak_ptr 本身不能直接调用接口方法,必须先 lock() 成 shared_ptr 确认对象还活着。忘记判活、直接对 weak_ptr 解引用是编译错误,但如果退化成裸指针存储,同样的疏忽会变成运行时的未定义行为。

卡片 3:双向持有时,至少一个方向要是弱引用

拉模型下 Observer 反向持有 Subject 很常见。如果 Subject 强引用 Observer、Observer 又强引用 Subject,两者会陷入循环引用,谁都不会被析构。规则很简单:一对相互引用的对象,至少一个方向必须是 weak_ptr 或裸指针。

卡片 4:通知过程中增删观察者,要小心迭代器失效

如果某个观察者在自己的 update() 里调用 removeObserver() 注销自己,或者新增了一个观察者,而 notifyObservers() 正在遍历同一个 observers_,就有迭代器失效的风险。稳妥的做法是通知前先拷贝一份快照再遍历,或者用索引而不是迭代器遍历。


九、本章小结

观察者模式最值得带走的,不是 Observer 这个接口名,而是一种拆分"状态"与"响应状态"的思路:

  1. 找出一个对象状态变化后,可能需要通知的、数量不固定的响应者。
  2. 为这些响应者定义统一的通知接口,让广播者不认识任何具体实现。
  3. 广播者只负责"发出通知",不负责决定响应者的生死------这一点在 C++ 里必须靠 weak_ptr 明确表达出来,Java 的 GC 不会替你兜底。
  4. 双向持有时,永远留一个方向的弱引用,避免循环引用。
  5. 简单的响应逻辑可以用 std::function 和 Lambda(Signal/Slot),复杂状态、需要自动生命周期管理的响应者,保留接口类实现。

回到本章标题的那句话:广播者该不该管听众的生死?答案是不该------但"不该管"不等于"不用管",而是要用 weak_ptr 把这层关系写进类型系统,让编译器和运行时都清楚谁对谁的生命周期负责。


附:编译版本说明

  • std::function、Lambda、std::weak_ptr、std::enable_shared_from_this:C++11。
  • std::make_shared:C++11。
  • 本文完整示例建议使用:
bash 复制代码
g++ -std=c++14 -Wall -Wextra -pedantic observer.cpp -o observer

如果项目使用 C++17、C++20 或更高标准,文中的写法同样适用。

相关推荐
ychqsq2 小时前
206.归程
经验分享·职场和发展
YYYing.2 小时前
【设计模式系列 (九) 】装饰器模式
c++·后端·设计模式·装饰器模式·c/c++
elseif1232 小时前
【2026 CSP-J】【逐题精讲】超详细(15道选择,3道阅读,2道完善)
java·开发语言·c++·算法·csp
殷色玫瑰3 小时前
C语言编译和链接:从 .c到 .exe,程序到底经历了什么?
linux·c语言·c++·算法
keep intensify3 小时前
C++20线程池
c++·算法·c++20
汉克老师3 小时前
GESP2026年9月认证C++一级( 第三部分编程题(2、棋盘上的奖赏))精讲
c++·gesp·小学生·学c++编程
努力努力再努力wz3 小时前
【CUDA 入门系列】核函数与线程模型:一文理清 Grid、Block、dim3、内置变量与全局索引
c++·架构
坤坤子吖3 小时前
Python基础语法学习:条件、循环与程序控制
开发语言·笔记·python·学习
ShineWinsu3 小时前
对于Redis:ZSet类型的解析
数据库·c++·redis·缓存·面试·有序集合·zset