[我正在研究Head First Design Patterns中的设计模式,为了变得自信,我计划在学习相应的章节之后,用C ++实现每个模式。
关于观察者模式,我确实在努力超越语言所主导的主要思想。
我一直在浏览以下内容:
但是,当我开始使用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
方法,该方法迫使派生类使用特定的signature重载该方法(在Java中似乎差不多);但是,这并没有说明观察者是否应该了解比“二进制”信息更多的信息(什么/什么都没有改变);确实,这是牵拉或推挤设计的决定,一方面,对我而言,这看起来像是一个实现细节。另一方面,该决定会影响应如何声明update
纯虚拟方法(选择参数列表),以及具体的观察者是否应持有指向被观察对象的指针/引用。这就是说,如果给了我两个抽象类Observer
和Observable
,则已经选择了实现方式,并且我不能更改。
关于Observable
接口,该书说它只需要提供addObserver
,removeObserver
和notifyObservers
的声明。但是,Observer
的集合必须是具体观察者类的成员。我了解选择集合(std::vector
,std::list
,...)是实现细节,但是集合some kind必须在Observable
中的事实看起来并不像详情。但是,一旦尝试对上述三个成员函数进行编码,就必须以一种可观察的具体方式选择一个集合。也许就够了,我不知道。
Observer::update
纯虚拟方法应该是公共的,以便具体的可观察对象实现notifyObservers
。但是具体的观察者可以将其update
的实现私有化。这样做对我来说很有意义:如果update
和notifyObservers
是每个观察者可观察关系的两端,那么如果观察者的update
具有以下条件,为什么调用者代码应该能够调用观察者的notifyObservers
不决定这样做吗?好吧,也许出于同样的原因,观察者可以处理自己的(取消)订阅(如果它持有被观察对象的句柄)。
特定于C ++的疑问:
可观察类(抽象基或派生的具体对象)是否应具有原始指针或智能指针的集合?这种选择的含义可能是什么?
addObserver
和removeObserver
应该采用Observer
参数。我认为不应通过值传递来避免复制;也许甚至都不用指针,否则在呼叫站点,我们必须传递&obj
而不是obj
。然后通过引用;但是哪个呢?一个const
左值引用将允许传递临时观察者,但这有意义吗?如果是的话,那么对rvalues和lvalues来说应该有两个重载,因为虚函数不允许具有通用引用的一个模板函数。
在我的示例代码中,我已经在具体的onservers类之一中存储了对具体可观察对象的引用,以便可以在getSpread
实现中使用特定于具体可观察对象(update
)的成员。我担心这可能不好。
Obesever应该具有update(或onChange或类似的东西)方法。 Obesever几乎总是需要有关更改的上下文来自我更新。因此,此处出现“推与拉”问题。在推送中,可观察必须将上下文信息有效负载传递给观察者。但是现在出现了一个问题,满足所有观察者的有效载荷结构应该是什么?当添加新的观察者时,有效载荷结构将如何演变,该观察者需要一些上下文参数,而该上下文参数不是当前有效载荷结构的一部分?另一方面,在拉动设计中,观察者需要查询可观察到的新状态。这意味着可以观察到的需要向观察者公开适当的接口。推与拉之间的选择取决于用例。推送提供了更好的解耦,但是更新上下文信息应该足够简单。在拉方法中,观察者可以查询可观察对象并获取更复杂和自定义的状态信息,即使不同的观察者也可以以不同的方式查询可观察对象。但是拉法在观察者和可观察者之间具有斯托格纳耦合。
观察者集合的三个主要用例是addObserver,removeObserver和notifyObservers。因此,任何无序集合都可以。
更新是一种通知方式,因此它必须在公共界面中可用。
特定于C ++
很多问题,但这是我的一些观点
我更喜欢将它们视为客户端和服务器,但这只是我想的。
观察者模式的基本思想是,客户(观察者)对服务器中的某些内容感兴趣(可观察),请订阅该内容事件。服务器为客户端提供客户端必须实现的接口然后客户端将该实现传递给服务器。这基本上是当您执行addObserver时会做什么。然后服务器将调用该方法(或方法)在界面中描述。这是服务器的责任决定观察者应该执行什么]
服务器具有另一个接口来处理本身实现的订阅这些是您称为addObserver和removeObserver的函数。
接口可以写成简单的结构
struct IClient
{
virtual void update(/*some argumet*/) = 0;
virtual ~IClient() = default;
}
class Observer : IClient
{
...
}
在客户端/服务器内部,如何表示数据取决于实现者,但通常是智能指针是WTG。
关于引用,我会改用shared_ptr并传递weak_ptr到服务器,或者将对象移动到服务器。