c++ - 超越基本思想理解 C++ 中的观察者模式
问题描述
我正在学习Head First Design Patterns中的设计模式,为了获得信心,我计划在学习完相应的章节后用 C++ 实现每个模式。
关于观察者模式,我真的在努力超越语言独立的主要思想。
我一直在浏览以下内容:
- 观察者模式在 C++14 中的实现
- 观察者模式实现
- 这个
- 那
- 实际上,还有其他几个。
但是,一旦我开始用 C++ 编码,一些特定于语言的困难就暴露了我对整个主题的一些误解,我无法用上面的点赞来解决。但是,我在这里发帖,因为我有一个(看似)工作代码。
示例代码如下,之后我列出了我对这个模式的理解和使用的一些担忧。
#include <algorithm>
#include <iostream>
#include <unordered_map>
#include <unordered_set>
class Observer {
public:
virtual void update() = 0;
};
class Observable {
protected:
std::unordered_set<Observer*> observers;
public:
virtual void addObserver(Observer&) = 0;
virtual void removeObserver(Observer&) = 0;
virtual void notifyObservers() = 0;
};
class Virus : public Observable {
public:
void addObserver(Observer& o) {
observers.insert(&o);
};
void removeObserver(Observer& o) {
std::erase_if(observers, [&o](auto const& io){ return &o == io; });
};
void notifyObservers() {
for (auto& o : observers) o->update();
};
void operator++() {
++spread;
std::cout << "\nLevel: " << spread
<< "\nSending notifications:\n";
notifyObservers();
}
int getSpread() { return spread; };
private:
int spread = 0;
};
class NormalCountry final : public Observer {
private:
void update() override {
if (obs.getSpread() < 2)
std::cout << "NormalCountry: What!? Coronavirus?\n";
else
std::cout << "NormalCountry: Ok, let's quarantine...\n";
};
private:
Virus& obs;
public:
void selfUnsubscribe() {
obs.removeObserver(*this);
};
void selfSubscribe() {
obs.addObserver(*this);
};
NormalCountry() = delete;
NormalCountry(Virus& o) : obs(o) {
selfSubscribe();
}
};
class BraveCountry final : public Observer {
public:
void update() override {
std::cout << "BraveCountry: No worries, people, our antibodies are cooler!\n";
};
};
int main() {
Virus cv;
NormalCountry it(cv);
BraveCountry uk;
++cv;
cv.addObserver(uk);
++cv;
it.selfUnsubscribe();
++cv;
}
对模式本身的怀疑:
我的理解是,观察者只需要能够通过
Observable
他们观察到的东西被告知其中发生了一些变化,这很容易将其Observer
视为一个接口,在 C++ 中,它意味着一个抽象类,只定义一个纯虚update
方法,它强制派生类使用该特定签名重载该方法(这在 Java 中似乎或多或少相同);然而,这并没有说明观察者是否应该知道比“二进制”信息更多的信息(有/没有改变);确实,这里是拉或推设计决策,一方面,对我来说,这看起来像是一个实现细节;另一方面,该决定会影响update
应该声明纯虚方法(选择参数列表),以及具体的观察者是否应该持有一个指向被观察对象的指针/引用。这就是说,如果给了我两个抽象类Observer
和Observable
,则已经选择了实现,我无法更改。至于
Observable
接口,书中说它只需要为addObserver
、removeObserver
和提供声明notifyObservers
。但是Observer
s 的集合必须是具体观察者类的成员。我知道集合(std::vector
,std::list
, ...)的选择是一个实现细节,但是某种集合必须在其中的Observable
事实看起来并不像一个细节。但是,一旦尝试编写上述三个成员函数,就必须在具体的 observable 中选择一个集合。也许这就够了,我不知道。Observer::update
为了实现具体的可观察对象,纯虚方法应该是公开的notifyObservers
。但是具体的观察者可以使他们的实现update
私有化。这样做对我来说有点道理:如果update
并且是每个观察者-可观察关系的两端,如果可观察者没有决定这样做notifyObservers
,为什么调用者代码可以调用观察者?好吧,也许出于同样的原因,观察者可以处理自己的(取消)订阅(如果它持有被观察对象的句柄)。update
notifyObservers
C++ 特有的疑问:
可观察类(抽象基类或具体派生类)应该具有原始指针或智能指针的集合吗?这种选择的影响可能是什么?
addObserver
并且removeObserver
应该带一个Observer
参数。我认为它不应该通过值传递,以避免复制;甚至可能不是通过指针,否则在调用站点我们必须通过&obj
而不是obj
. 然后是参考;但哪个?左值引用将const
允许传递临时观察者,但这有意义吗?如果是这样,那么应该对右值和左值有两个重载,因为虚函数不允许有一个具有通用引用的模板函数。在我的示例代码中,我在具体的 onservers 类之一中存储了对具体可观察对象的引用,以便我可以
getSpread
在实现中使用特定于具体可观察对象 ( ) 的一些成员update
。我担心这可能很糟糕。
解决方案
- 关于观察者界面
Obesever 应该有更新(或 onChange 或类似的东西)方法。Obesever 几乎总是需要一些有关更改的上下文来更新自身。因此这里出现了推与拉的问题。在 push 中,observable 必须将上下文信息负载传递给观察者。但是现在问题来了,什么应该是满足所有观察者的有效载荷结构?当添加新的观察者需要一些不属于当前有效载荷结构的上下文参数时,有效载荷结构将如何演变?另一方面,在拉式设计中,观察者需要向可观察者查询新状态。这意味着 observable 需要向观察者公开适当的接口。推与拉之间的选择取决于用例。Push 提供了更好的解耦,但更新上下文信息应该足够简单。在拉式方法中,观察者可以查询 observable 并获得更复杂和自定义的状态信息。即使不同的观察者也可以以不同的方式查询 observable。但是 pull 方法在观察者和可观察者之间有更紧密的耦合。
- 关于 observable 中观察者的集合
观察者集合的三个主要用例 addObserver、removeObserver 和 notifyObservers。所以任何无序的集合都应该没问题。
- 关于观察员更新公开
update 是一种通知方式,因此必须在公共界面中可用。
C++ 特定
- Observable 应该保持弱引用(不延长观察者生命周期)观察者。
- 在观察者中保持对 observable 的引用:这不是一个好主意。它可以看作是 pull 方法的隐式实现。但是保留界面会比保留具体对象更好。
推荐阅读
- json - PowerShell 检索 MS Graph 3 级数据返回非 json 结果
- r - initial_probs 的长度不等于状态数
- sql-server - 当 JPA 事务通过网络失败时,sql server 会做什么?
- javascript - 循环通过标签并通过 .click 更改 url
- java - Jsoup 检查标签是否存在
- android - Plugin.Geolocator 不可用
- php - 从字符串中获取特定字符串,以模式开头
- docker - 如何在 Docker 之外将 NGINX 反向代理到 proxy_pass 到 docker 容器
- elasticsearch - 无法使用 _delete_by_query 删除 Elasticsearch 中的项目
- javascript - JavaScript 清理 HTML 字符串并删除 ID、类和其他属性