C++ 设计模式 十九:观察者模式 (读书 现代c++设计模式)

观察者

文章目录

近期在学 GUI 编程( Qt), 所以今天跳读, 先把第19类设计模式(观察者模式)给看了.

观察者模式的核心应用场景是 实现对象间的一对多依赖关系,当被观察对象(Subject)的状态变化需要自动通知多个依赖对象(Observer)时使用。

相较于其他语言, c++ 标准库并没有给出现成实现, 实现一个安全的、正确的观察者(如果存在这样的东西的话)从技术上来说是比较复杂的.

属性观察者

现在是我们的需求: 当人长大一岁, 我们要为她庆祝生日, 为了实现, 我们先给出定义:

cpp 复制代码
struct Person {
	int age_;

	explicit Person(const int age) : age_{age} {}
};

但是很显然, 现在很难知道一个人的年龄在何时变化. 为了获取这个消息, 可以采用轮询, 比如每100ms读一次年龄, 然后和之前的年龄比较.不过这方法显然扩展性不够好, 我们采取一些别的方法.

当一个改变年龄字段的写操作发生时,我们想要捕获这一消息。捕捉这个通知的的唯一方法是创建setter

cpp 复制代码
struct Person {
private:
	int age_;

public:
	explicit Person(const int age) : age_{age} {}
	~Person() = default;

	[[nodiscard]] int get_age() const {
		return age_;
	}
	void set_age(const int age) {
		age_ = age;
	}
};

现在我们为年龄创建了一个 setterset_age(), 至于如何实现通知, 我们需要一些其他的类.

观察者 Observer

第一种方式, 我们定义一个接口, 任何需要获取 Person 对象变化信息的人都要给出实现, 我们将接口实现如下:

cpp 复制代码
struct PersonListener {
	virtual void person_changed(Person& p, const string& property_name,
	                            const int any_new_value) = 0;
};

不过这不够通用, 如果对于每一种类型我们都去实现一个 Listener 实在麻烦, 所以我们用上泛型.

cpp 复制代码
template <typename T>
struct Observer {
	virtual ~Observer() = default;

	virtual void field_changed(T& source, const string& field_name) = 0;
};

这里的函数 field_changed() 就是我们的虚函数接口了, 其中的两个入参就和他们的名字一样, source 是对要更改字段的引用, field_name 就是要更改字段的名称, 此处是作为字符串传入的(不过这样做会损害代码重构性).

作者注1:c#在连续的版本中两次明确地解决了这个问题。首先,它引入了一个名为CallerMemberName的属性,该属性将调用函数/属性的名称作为参数的字符串值插入。第二个版本简单地引入了nameof(Foo),它将取符号的名称并将其转换为字符串。

好的, 现在这样实现之后, 我们就可以观察 Person 类的变化了, 第一个使用方式: 将他们写到终端:

cpp 复制代码
struct ConsolePersonObserver final : public Observer<Person> {
	void field_changed(Person& source, const string& field_name) override {
		cout << "Person's " << field_name << "has changed to " << source.get_age()
			 << "." << endl;
	}
};

通过继承不同类型的 Observer , 我们就能获取多个不同类的变化, 例如引入一个 Creature 类:

cpp 复制代码
#include <iostream>
#include <string>

using namespace std;

struct Person {
private:
	int age_;

public:
	explicit Person(const int age) : age_{age} {}
	~Person() = default;

	[[nodiscard]] int get_age() const {
		return age_;
	}

	void set_age(const int age) {
		age_ = age;
	}
};

struct Creature {
private:
	int attack_{0};
	int defense_{0};

public:
	explicit Creature() {}

	[[nodiscard]] int get_attack() const {
		return attack_;
	}
	void set_attack(const int attack) {
		attack_ = attack;
	}
	[[nodiscard]] int get_defense() const {
		return defense_;
	}
	void set_defense(const int defense) {
		defense_ = defense;
	}
};

template <typename T>
struct Observer {
	virtual ~Observer() = default;

	virtual void field_changed(T& source, const string& field_name) = 0;
};


struct ConsolePersonObserver final : public Observer<Person> {
	void field_changed(Person& source, const string& field_name) override {
		cout << "Person's " << field_name << "has changed to " << source.get_age()
			 << "." << endl;
	}
};

struct ConsolePersonCreatureObserver final : Observer<Person>, Observer<Creature> {
	void field_changed(Person &source, const string &field_name) override {}
	void field_changed(Creature &source, const string &field_name) override {}
};

按照作者所说:

另一种替代方法是使用std::any,并去掉泛型实现。试一试!

这里给出一个实现(std::any要求标准在c++17及以上):

cpp 复制代码
#include <iostream>
#include <string>
#include <any>

using namespace std;

struct Person {
private:
	int age_;

public:
	explicit Person(const int age) : age_{age} {}
	~Person() = default;

	[[nodiscard]] int get_age() const {
		return age_;
	}

	void set_age(const int age) {
		age_ = age;
	}
};

struct Creature {
private:
	int attack_{0};
	int defense_{0};

public:
	explicit Creature() {}

	[[nodiscard]] int get_attack() const {
		return attack_;
	}

	void set_attack(const int attack) {
		attack_ = attack;
	}

	[[nodiscard]] int get_defense() const {
		return defense_;
	}

	void set_defense(const int defense) {
		defense_ = defense;
	}
};

struct Observer {
	virtual ~Observer() = default;
	virtual void field_changed(any& source, const string& field_name) = 0;
};


struct ConsolePersonObserver final : public Observer {
	void field_changed(any& source, const string& field_name) override {
		if (const auto p = any_cast<Person*>(source)) {
			cout << "Person's " << field_name << "has changed to " << p->get_age()
				<< "." << endl;
		}
	}
};

struct ConsolePersonCreatureObserver final : Observer {
	void field_changed(any& source, const string& field_name) override {
		if (auto p = any_cast<Person*>(source)) {
			// 处理Person逻辑
		} else if (auto c = any_cast<Creature*>(source)) {
			// 处理Creature逻辑
		}
	}
};

总体思路就是引入 std::any 之后可以去掉 Observer 的泛型依赖, 将类型判断从编译时(继承)转移到了运行时(具体的 Observer 内部做检测).

被观察者 Observable

现在来到了关键问题: 我们如何让观察者和被观察者之间进行通信?

我们这样想, 如果我们让观察者来实现消息获取机制, 那么对于每一个观察者来说都要实现一个获取函数, 同时我们不得不采用轮询方式向被观察者发送请求信息, 显然这不是个好设计.

那么我们倒置一下职责, 让被观察者来实现消息机制, 也就是被观察者一旦发生了变化立马向观察者传达自身变化了的信息.

在我们的例子里, 被观察者就是 Person 所以我们回到这个类, 对他做一些修改.

  • 为了提醒所有观察者, 我们在被观察者内部要维护一个观察者集合
  • 让观察者可以订阅或者取消订阅 subscribe()/unsubscribe() 发生在Person上的变化。
  • 当Person发生改变的时候通知所有的观察者

为此我们定义一个被观察者要实现的接口:

cpp 复制代码
template <typename T>
struct Observable {
private:
	std::vector<std::weak_ptr<Observer<T>>> observers_; // 存储弱引用

public:
	void subscribe(std::shared_ptr<Observer<T>> f) {
		observers_.emplace_back(f); // 自动转换为 weak_ptr
	}

	void unsubscribe(const std::shared_ptr<Observer<T>>& f);

	void notify(T& source, const string& name);
};

首先看到了我们的类 Observable , 这就是我们的抽象被观察者

  • 这是一个模板类, 为了接受不同类型的 Observer 而设计.
  • 然后是数据成员, 我们用一个 vector 来维护所有的观察者, vector 内是不同 Observer 的指针, 由于被观察者通常不持有观察者的所有权, 所以我们用 weak_ptr 来管理生命周期. 同时因为不持有观察者的所有权, 所以将访问限制为私有, 使得外界均无法访问,
  • 函数 subscribe() 用以添加新的观察者, 参数为 shared_ptr, 添加时自动转换为 weak_ptr.

然后是 notify() 函数和 unsubscribe() 函数的实现:

cpp 复制代码
template <typename T>
struct Observable {
private:
	std::vector<std::weak_ptr<Observer<T>>> observers_; // 存储弱引用

public:
	void subscribe(std::shared_ptr<Observer<T>> f) {
		observers_.emplace_back(f); // 自动转换为 weak_ptr
	}

	void unsubscribe(const std::shared_ptr<Observer<T>>& f) {
		observers_.erase(
			std::remove_if(observers_.begin(), observers_.end(),
				[&](const auto& weak_obs) {
					auto obs = weak_obs.lock();  // 尝试升级为 shared_ptr
					return !obs || obs == f;     // 清理失效或匹配的观察者
				}),
			observers_.end()
		);
	}

	void notify(T& source, const string& name) {
		auto it = observers_.begin();
		while (it != observers_.end()) {
			if (auto observer = it->lock()) {
				observer->field_changed(source, name);
				++it;
			} else {
				it = observers_.erase(it); // 自动清理失效观察者
			}
		}
	}
};

notify() 函数用于订阅, it为当前迭代器, 循环体内 lock()尝试获取有效 shared_ptr, 成功时执行通知并前进迭代器, 失败时擦除当前元素并获取新迭代器.

unsubscribe() 函数采用双重清理, weak_obs.lock() 检查观察者是否已失效, obs == f 匹配具体要移除的观察者, 使用 erase-remove 惯用法保证容器操作安全.

现在我们完成了抽象观察者类, 继续下去, 做出具体实现, 新的Person 类和 get_age() 函数将实现如下:

cpp 复制代码
struct Person : Observable<Person> {
private:
	int age_;

public:
	explicit Person(const int age) : age_{age} {}
	~Person() = default;

	[[nodiscard]] int get_age() const {
		return age_;
	}

	void set_age(const int age) {
		if (this->age_ != age) {
			age_ = age;
			notify(*this, "age");
		}
	}
};

set_age() 中调用 notify() 为所有的观察者做出通知.


连接观察者和被观察者

现在我们要使用我们之前构建的观察者和被观察者, 我们的具体观察者如下:

cpp 复制代码
struct ConsolePersonObserver final : public Observer<Person> {
	void field_changed(Person& source, const string& field_name) override {
		cout << "Person's " << field_name << "has changed to " << source.get_age()
			 << "." << endl;
	}
};

他和之前我们的实现是一样的, 在这里只是再次给出,

然后就是测试用例:

cpp 复制代码
void test() {
	Person p{20};
	const auto cpo = make_shared<ConsolePersonObserver>();
	p.subscribe(cpo);
	p.set_age(21);
	p.set_age(22);
}

将输出如下:

Person's agehas changed to 21.

Person's agehas changed to 22.

依赖问题

我们继续引入一些问题, 假设有一个选举需求.

大于16岁的人具有选举权,因此当某个人具有选举权之后我们希望被通知到。首先,我们假设Person类有如下的getter函数:

cpp 复制代码
bool get_can_vote() const { 
	return age_ >= 16;
}

显而易见这个函数依赖于 age_ 而触发, 所以我们如果按照之前的想法, 为他设定一个 setter 这并没有必要, 我们可以将这个操作同步到之前关于 agesetter 也就是 age_setter中一起解决掉.

然后我们再给出 Person 类的新实现:

cpp 复制代码
struct Person : Observable<Person> {
private:
	int age_;

public:
	explicit Person(const int age) : age_{age} {}
	~Person() = default;

	[[nodiscard]] int get_age() const {
		return age_;
	}

	bool get_can_vote() const { return age_ >= 16; }

	void set_age(const int value) {
		if (age_ != value) {
			const auto old_can_vote = get_can_vote(); // store old value

			age_ = value;
			notify(*this, "age");

			if (old_can_vote != get_can_vote()) // check value has changed
				notify(*this, "can_vote");
		}
	}
};

这样看来 set_age 似乎长得过于臃肿了。我们不仅检查年龄是否改变,同时也检查can_vote是否改变,并给出通知.

可能会认为这种方法不能很好地扩展, 想象一下 can_vote 依赖于两个字段,比如 agecitizenship ------ 这意味着这两个属性的 setter 都必须处理 can_vote 通知。

更糟糕的是,如果年龄也会以这种方式影响其他10种属性呢? 这是一个不可用的解决方案,它会导致脆弱的代码无法维护,因为变量之间的关系需要手动跟踪。

坦白地说,在前一种情况下,can_vote 属性依赖 age 属性。依赖性属性的挑战本质上是 Excel 等工具的挑战:给定不同单元格之间的大量依赖性,当其中一个单元格发生变化时,您如何知道哪些单元格需要重新计算。

当然,属性依赖关系可以被形式化为某种类型的map<string, vector<string>>。这将保留一个受属性影响的属性列表(或者相反,影响属性的所有属性)。但是,这个map必须手工定义,而且要与实际代码保持同步非常麻烦。

取消订阅和线程安全

现在回头看取消订阅的实现:

cpp 复制代码
void unsubscribe(const std::shared_ptr<Observer<T>>& f) {
	observers_.erase(
		std::remove_if(observers_.begin(), observers_.end(),
		               [&](const auto& weak_obs) {
			               auto obs = weak_obs.lock(); // 尝试升级为 shared_ptr
			               return !obs || obs == f;    // 清理失效或匹配的观察者
		               }),
		observers_.end()
	);
}

我们采用了算法 erase-remove_if, 对于 vector 这并不线程安全, 同时调用 subscribe()unsubscribe() 可能会导致意想不到的结果,因为这两个函数都会修改 vector.

当然一个易行的解决方案是加锁:

cpp 复制代码
template <typename T>
struct Observable {
	void notify(T& source, const string& name) {
		scoped_lock<mutex> lock{mtx};
		...
	}

	void subscribe(Observer<T>* f) {
		scoped_lock<mutex> lock{mtx};
		...
	}

	void unsubscribe(Observer<T>* o) {
		scoped_lock<mutex> lock{mtx};
		...
	}

private:
	vector<Observer<T>*> observers;
	mutex mtx;
};

另一个非常可行的替代方案是使用类似TPL/PPLconcurrent_ vector。当然, 会失去排序保证(换句话说,一个接一个地添加两个对象并不能保证它们按照那个顺序得到通知),但不必自己管理锁。

可重入

最后一种实现提供了一些线程安全性,只要有人需要,就锁定这三个关键方法中的任何一个。但是现在让我们设想以下场景:您有一个交通管理组件,它一直监视一个人,直到他到了可以开车的年龄。当他们17岁时,组件就会取消订阅:

cpp 复制代码
struct TrafficAdministration final : Observer<Person> {
	void TrafficAdministration::field_changed(Person& source,
	                                          const string& field_name) override {
		if (field_name == "age") {
			if (source.get_age() < 17)
				cout << "Whoa there, you are not old enough to drive!\n";
			else {
				// oh, ok, they are old enough, let's not monitor them anymore
				cout << "We no longer care!\n";
				const auto temp = make_shared<TrafficAdministration>();
				source.unsubscribe(temp);
			}
		}
	}
};

这将会出现一个问题,因为当某人17岁时,整个调用链将会是:

notify() ---> field_changed() ---> unsubscribe()

这是一个问题,因为在unsubscribe()中,我们最终试图获取一个已经被获取的锁。这是一个可重入问题。处理这件事有不同的方法:

  • 一种方法是简单地禁止这种情况。毕竟,至少在这个特定的例子中,很明显这里发生了可重入性
  • 另一种方法是放弃从集合中删除元素的想法。相反,我们可以这样写:
cpp 复制代码
void unsubscribe(Observer<T>* o) {
	auto it = find(observers_.begin(), observers_.end(), o);
	if (it != observers_.end())
		*it = nullptr; // cannot do this for a set
}

随后,当使用notify()时,只需要进行额外的检查:

cpp 复制代码
void notify(T& source, const string& name) {
	for (auto&& obs : observers_)
		if (obs)
			obs->field_changed(source, name);
}

通过 Boost.Signals2 来实现 Observer

然后作者又是很经典地用了 boost 库, 这里直接贴原文

观察者模式有很多预打包的实现,并且可能最著名的是 Boost.Signals2 库。本质上,该库提供了一种称为信号的类型,它表示 C++ 中的信号术语(在别处称为事件)。可以通过提供函数或 lambda 表达式 来订阅此信号。它也可以被取消订阅,当你想通知它时,它可以被解除。

cpp 复制代码
template <typename T>
struct Observable {
  signal<void(T&, const string&)> property_changed;
};

它的调用如下所示:

cpp 复制代码
struct Person : Observable<Person> {
  void set_age(const int age) {
    if (this->age == age) return;
    this->age = age;
    property_changed(*this, "age");
  }
};

API 的实际使用将直接使用信号,当然,除非你决定添加更多 API 陷阱以使其更容易:

cpp 复制代码
Person p{123};
auto conn = p.property_changed.connect([](Person&, const string& prop_name) {
  cout << prop_name << " has been changed" << endl;
});
p.set_age(20);  // name has been changed
// later, optionally
conn.disconnect();

connect() 调用的结果是一个连接对象,它也可以用于在你不再需要信号通知时取消订阅。

总结

何时需要使用观察者模式?

观察者模式的核心应用场景是 实现对象间的一对多依赖关系,当被观察对象(Subject)的状态变化需要自动通知多个依赖对象(Observer)时使用。具体需求场景包括:

  1. 事件驱动系统
    • GUI框架中按钮点击事件、数据模型变化触发视图更新。
    • 示例:用户点击按钮后,通知多个UI组件刷新状态。
  2. 实时数据分发
    • 传感器数据更新时,通知多个监控模块(如温度传感器→显示屏、报警器)。
  3. 发布-订阅机制
    • 消息队列中生产者发布消息,多个消费者订阅并处理消息。
    • 示例:新闻推送系统,用户订阅主题后接收相关新闻。

观察者模式解决的核心问题
  1. 紧耦合问题
    • 避免被观察者直接调用具体观察者的方法,减少对象间的直接依赖。
  2. 动态通知机制
    • 支持运行时动态添加或移除观察者,增强系统灵活性。
  3. 状态同步
    • 确保多个观察者与被观察者的状态保持一致,避免手动同步的复杂性。

与其他设计模式的协同使用

观察者模式常与其他模式结合,以优化系统设计:

模式 协同场景 示例
中介者模式 通过中介者统一管理观察者和被观察者的交互,降低对象间直接通信的复杂度。 聊天室中,用户(观察者)的消息由中介者广播给其他用户,而非直接互相引用。
状态模式 被观察者的状态变化触发观察者的行为调整,两者结合实现状态驱动的响应逻辑。 订单状态变为"已发货"时,通知物流系统和用户界面更新显示。
命令模式 将观察者的响应操作封装为命令对象,支持撤销、重做等扩展功能。 用户点击按钮(触发命令对象),命令执行后通知日志模块记录操作。
装饰器模式 动态增强观察者的功能(如添加日志、缓存),而无需修改被观察者代码。 日志观察者被装饰器包装,在记录日志前添加时间戳或IP信息。

与其他模式的对比
  • 发布-订阅模式
    • 观察者模式:被观察者直接维护观察者列表,无中间事件通道(强耦合)。
    • 发布-订阅模式:通过事件总线解耦发布者和订阅者(松耦合)。
  • 中介者模式
    • 中介者模式:集中管理对象间交互,避免多对多通信。
    • 观察者模式:实现一对多的单向通知机制。

经典应用场景
  1. MVC架构
    • 模型(Model)作为被观察者,视图(View)作为观察者,模型数据变化时自动更新视图。
  2. 实时监控系统
    • 服务器资源(CPU、内存)状态变化时,通知监控仪表盘和报警系统。
  3. 股票交易系统
    • 股票价格变动时,通知交易算法、用户界面和风险控制模块。
  4. 游戏引擎
    • 角色血量变化时,触发UI血条更新、音效播放和存档自动保存。

实现步骤与关键点
  1. 定义观察者接口
    • 声明更新方法(如update()),供具体观察者实现。
  2. 定义被观察者接口
    • 提供注册(attach())、注销(detach())和通知(notify())方法。
  3. 具体实现
    • 被观察者维护观察者列表,状态变化时调用notify()遍历列表触发观察者更新。
注意事项
  1. 内存管理
    • 被观察者持有观察者的指针时,需确保观察者生命周期合理,避免悬垂指针。
  2. 性能优化
    • 当观察者数量庞大时,通知操作可能成为性能瓶颈,可通过异步通知或批量处理优化。
  3. 循环依赖
    • 避免观察者和被观察者相互引用,导致内存泄漏或死锁。

观察者模式是 事件驱动的纽带,其核心价值在于:

  • 解耦与扩展性:分离事件源与事件处理器,支持动态添加观察者。
  • 实时响应:确保状态变化即时通知所有依赖对象。
  • 通用性:适用于GUI、监控、消息系统等广泛场景。

通过与其他模式(如中介者、状态)的协同,观察者模式能够构建高响应、低耦合的系统架构,是现代软件设计中实现事件处理的核心工具。

相关推荐
七七七七072 小时前
浅谈C++/C命名冲突
c语言·c++
茂茂在长安2 小时前
JAVA面试_进阶部分_23种设计模式总结
java·设计模式·面试
c-c-developer2 小时前
C++ Primer 特定容器算法
c++
宇寒风暖3 小时前
侯捷 C++ 课程学习笔记:Spaces in Template Expression、 nullptr and stdnull
开发语言·c++·笔记·学习
明月看潮生4 小时前
青少年编程与数学 02-010 C++程序设计基础 13课题、数据类型
开发语言·c++·青少年编程·编程与数学
Awkwardx4 小时前
C++初阶—list类
开发语言·c++
2501_902556234 小时前
C++ 中 cin 和 cout 教程
数据结构·c++
萌の鱼4 小时前
leetcode 73. 矩阵置零
数据结构·c++·算法·leetcode·矩阵
小王子10245 小时前
设计模式Python版 观察者模式(上)
python·观察者模式·设计模式