本文内容来源于B站:
【「观察者模式」与「发布/订阅模式」,你分得清楚吗?】
文章目录
- 观察者模式(Observer Pattern)的代码优化
- 观察者模式 与 发布订阅模式 他们是一样的吗?
- 发布订阅模式
- 总结
我们想象这样一个场景:
我们需要开发一个软件系统,该系统可以实时接收某支股票的最新价格,系统可以控制,将这个价格展示在路边的电子广告牌上,同时,坐在控制室中的管理人员可以在显示器上看到这个价格。
当每支股票价格发生变化时,电子广告牌和显示器上的股票价格也要实时变化。
我们首先使用C++来模拟他的一种可能的实现方式:
- 引入必要的头文件;
- 创建一个 Monitor 类来表示显示器,其中包含 print 方法来更新显示器的显示内容;
- 创建一个 Billboard 类来表示广告牌,同样已有 print 方法来进行更新;
- 创建 Stock 类,该类封装了与股票相关的数据和操作,它包含有一个表示当前股票价格的成员变量
price
,以及一个用于更改其值的成员方法 setPrice。- 既然 Stock 类是通过 setPrice 来更新股票价格的,那么我们也可以在这里调用 Monitor 和 Billboard 类中的接口来同步更新广告牌和显示器上的数据,所以我们在 Stock 类找那个加入这两个类中的指针,来实现接口的调用。
- 需要补充的是,我们需要在 Stock 类中初始化这里的两个类指针,让他们指向已经创建好的 Monitor 和 Billboard 类对象。
#include <iostream>
struct Monitor {void print(int v) {std::cout << "Monitor:" << v;}
};
struct Billboard {void display(int v) {std::cout << "Billboard:" << v;}
};
struct Stock {int price = 20;Monitor* monitor;Billboard* billboard;Stock(Monitor* monitor, Billboard* billboard): monitor(monitor), billboard(billboard)void setPrice(int v) {price = v;monitor->print(price);billboard->print(price);}
};
随后我们就可以调用 Stock 对象上的 setPrice 方法来更新股票价格,并且广告牌与显示器上的数字也会被相应更新。
int main () {Monitor monitor;Billboard billboard;Stock stock {&monitor, &billboard};stock.setPrice(10);
}
但是这样的实现方式有许多的问题:
- 类之间的紧耦合:Stock 类的稳定性依赖于 Monitor 与 Billboard 类接口的稳定,而当这些接口的名称或使用方式发生变化时,那么 Stock 类就需要被同时修改;并且同样的情况也发生在有更多的显示媒介加入时,比如软件希望同时支持在手机APP上显示最新的股票价格,这个时候我们也需要再次修改 Stock 类。
- 这样的紧耦合使得代码的维护成本变得很高,那么如何解决呢?答案就是观察者模式!
观察者模式(Observer Pattern)的代码优化
也就是我们的 Monitor 类和 Billboard 类都是观察者,这里我们可以提供一个统一的父类 Observer ,并且在里面写入 update 接口,这个接口将所有观察者在观测时发生的不同动作进行了统一的包装。Observer 的构造和析构会在后面进行补全:
#include <iostream>
struct Observer {Observer();virtual ~Observer();virtual void update(int) = 0;
}
struct Monitor {void print(int v) {std::cout << "Monitor:" << v;}
};
struct Billboard {void display(int v) {std::cout << "Billboard:" << v;}
};
然后我们的 Monitor 类的构造函数也做相应的修改,然后我们在 Monitor 类中实现父类的 update 接口,Billboard 观察者类也同理,所有观察者都需要将当观测数据发生变化时,可能发生的行为封装在统一的 update(int) 接口中。
struct Monitor {Monitor() : Observer() {}void print(int v) {std::cout << "Monitor:" << v;}void update(int v) override {print(v);}
};
struct Billboard {Billboard() : Observer() {}void display(int v) {std::cout << "Billboard:" << v;}void update(int v) override {display(v);}
};
下面我们把 Stock 类也加入到代码中,我们首先为他提供一个前置声明。接着我们在 Observer 类中加入一个指向 Stock 类对象的指针引用,随后我们改写 Monitor 和 Billboard 类的构造函数,接着我们将之前那段 Stock 类的定义原封不动得放入到代码中。
#include <iostream>struct Stock;struct Observer {Stock* stock;Observer(Stock* stock);virtual ~Observer();virtual void update(int) = 0;
};
struct Monitor {Monitor(Stock* stock) : Observer(stock) {}void print(int v) {std::cout << "Monitor:" << v;}
};
struct Billboard {Billboard(Stock* stock) : Observer(stock) {}void display(int v) {std::cout << "Billboard:" << v;}
};struct Stock {int price = 0;void setPrice(int v) {price = v;}
};
在观察者模式下, Stock 类作为所有观察者关注的对象,它需要支持三个成员方法,notify(int) detach(Observer*) attach(Observer*),这三个方法会围绕 Stock 类内部维护的一个集合进行操作。这个集合中就存放有指向 Oberser 子类对象的指针。这些指针指向的就是所有对 Stock 类感兴趣的观察者对象。
#include list
struct Stock {int price = 0;std::list<Observer*> observerList;void attach(Observer* o) {observerList.push_back(0);}void detach(Observer* o) {observerList.remove(0);}void setPrice(int v) {price = v;}
};
这里我们的 notify(int v) 方法是观察者模式的核心,这个方法会遍历观察者指针容器,并以此调用每个观察者对象上的 update 方法。通过这种方式,Stock 类便能够在状态发生变化时及时通知所有对此感兴趣的观察者对象来进行相应的更新操作,最后我们在 setPrice 方法中调用 notify 方法,这样在股价发生变化时, 能够通知到所有观察者对象。
#include list
struct Stock {int price = 0;std::list<Observer*> observerList;void attach(Observer* o) {observerList.push_back(0);}void detach(Observer* o) {observerList.remove(0);}void notify(int v) {for (auto observer : observerList) {observer->update(v);}}void setPrice(int v) {price = v;notify(v);}
};
下一步我们需要补全 Observer 类的构造函数,让每一个观察者对象在完成构造后,都能被加入到某个 Stock 对象的观察者集合中。而当观察者对象被析构时则从相应的观察者集合中被移除。
Observer::observer(Stock* stk) : stock(stk) {stock->attach(this);
}
Observer::observer(Stock* stk) : stock(stk) {stock->detach(this);
}
最后在 main 函数中我们可以这样来使用这些类:
int main () {Stock stock;Monitor monitor { &stock };Billboard board { &stock };stock.setPrice(10);
}
我们可以看到当 Stock 类在构造时,不再依赖任何观察者对象,因此它的实现可以保持稳定。而观察者对象也可以自由选择观察目标,可以是不同的 Stock 对象,同时不同种类的观察者对象的横向扩展也变得更加灵活。
UML类图如上,并且在某些情况下我们可以进一步对观察者模式进行抽象。从而得到另一个版本的 UML 类图和另一个版本的代码实现。
我们为 Stock 类提供了独立的抽象接口 Subject ,从而将 attach、detach 以及 notify 这三个标准接口抽离出来。而 Stock 则保有自己的内部状态,为了保证状态访问的合法性,他们仅能够通过专有的访问器进行访问,下面是对应的 C++ 代码实现:
#include <iostream>
#include <list>struct Observer;
struct Subject {std::list<Observer*> observerList;virtual void attach(Observer* o) = 0;virtual void detach(Observer* o) = 0;virtual void notify() = 0;
};class Stock : public Subject {int price = 0;public:int getPrice();void setPrice(int);void attach(Observer* o) override;void detach(Observer* o) override;void notify() override;
};struct Observer {Subject* sub;Observer(Subject* sub);virtual ~Observer();virtual void update() = 0;
};struct Monitor : Observer {Monitor(Subject* sub) : Observer(sub) {}void print(int v) const { std::cout << "Monitor: " << v << std::endl; }void update() override { print(static_cast<Stock*>(sub)->getPrice()); }
};struct Billboard : Observer {Billboard(Stock* stock) : Observer(stock) {}void display(int v) const { std::cout << "Billboard: " << v << std::endl; }void update() override { display(static_cast<Stock*>(sub)->getPrice()); }
};int Stock::getPrice(void) { return price; }
void Stock::setPrice(int v) {price = v;notify();
}
void Stock::attach(Observer* o) { observerList.push_back(o); }
void Stock::detach(Observer* o) { observerList.remove(0); }
void Stock::notify() {for (auto observer : observerList) {observer->update();}
}Observer::Observer(Subject* sub) : sub(sub) { sub->attach(this); }Observer::~Observer() { sub->detach(this); }int main() {Stock stock;Monitor monitor{&stock};Billboard board{&stock};stock.setPrice(10);
}
观察者模式 与 发布订阅模式 他们是一样的吗?
发布订阅模式
发布订阅模式是一种常用的系统架构模式,他用来规定一个系统中的不同部分之间应该如何进行消息传递。
在这个模式中,发布者可以是系统中的消息服务、事件服务;而订阅者可以是系统中的其他服务,比如网关服务、路由服务等等。
发布者与订阅者之间互相不知道对方的存在,他们之间通过名为消息代理的部分连接起来。发布者会向消息代理发送不同类型的消息,比如一个系统通知、一个发生的事件。而订阅者则会与消息代理通信,选择自己想要订阅的消息类型。
在之后的流程中,每当消息代理接收到符合要求的消息,便会把他们直接转发给相应的订阅者进行处理;这便是发布订阅模式的基本形式。
总结
所以综合来看,我们可以得出这样的结论: