深入浅出设计模式——行为型模式之观察者模式 Observer
文章目录
- 1.观察者模式简介
- 2.观察者模式结构
- 3.观察者模式代码实例
- 3.0.公共头文件
- 3.1.观察者
- 3.1.1.抽象观察者Observer
- 3.1.2.具体观察者Player
- 3.2.目标类
- 3.2.1.抽象目标AllyCenter
- 3.2.2.具体目标AllyCenterController
- 循环包含
- 错误示例
- “前向声明什么时候不够、必须 #include 对方头?”
- 3.3.客户端代码示例及效果
- 4.观察者模式的应用
- 5.总结
代码仓库
观察者模式非常常见,近年来逐渐流行的响应式编程就是观察者模式的应用之一。观察者模式的思想就是一个对象发生一个事件后,逐一通知监听着这个对象的监听者,监听者可以对这个事件马上做出响应。
生活中有很多观察者模式的例子,比如我们平时的开关灯。当我们打开灯的开关时,灯马上亮了;当我们关闭灯的开关时,灯马上熄了。这个过程中,灯就对我们控制开关的事件做出了响应,这就是一个最简单的一对一观察者模式。当某某公众号发表一篇文章,所有关注了公众号的读者立即收到了文章,这个过程中所有关注了公众号的微信客户端就对公众号发表文章的事件做出了响应,这就是一个典型的一对多观察者模式。 这表明“更新发布文章”并不是孤立的,而是与众多对象产生了关联。一个对象行为的改变,其相关联的对象都会得到通知,并自动产生对应的行为。这在软件设计模式中,即是观察者模式。
再举个例子,比如警察一直观察着张三的一举一动,只要张三有什么违法行为,警察马上行动,抓捕张三。
众所周知,张三坏事做尽,是一个老法外狂徒了,所以不止一个警察会盯着张三,也就是说一个被观察者可以有多个观察者。当被观察者有事件发生时,所有观察者都能收到通知并响应。观察者模式主要处理的是一种一对多的依赖关系。 它的定义如下:
观察者模式(Observer Pattern):定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
1.观察者模式简介
软件系统中的对象并不是孤立存在的,一个对象行为的改变可能会引起其他所关联的对象的状态或行为也发生改变,即“牵一发而动全身”。观察者模式建立了一种一对多的联动,一个对象改变时将自动通知其他对象,其他对象将作出反应。观察者模式中,发生改变的对象称为“观察目标”,被通知的对象称为“观察者”。一个观察目标可以有很多个观察者。
观察者模式定义如下:
观察者模式:定义对象之间的一种一对多的依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象都得到通知并被自动更新。
观察者模式又被称为发布-订阅模式(Publish-Subscribe)、模型-视图模式(Model-View)、源-监听器模式(Source-Listener)、从属者模式(Dependents)。
2.观察者模式结构
观察者模式由观察者和观察目标组成,为便于扩展,两个角色都设计了抽象层。观察者模式的UML图如下:
下述是观察者模式的典型实现:
#define __DEMO_H__
#ifdef __DEMO_H__using namespace std;
#include <list>// 抽象观察者
class Observer
{
public:virtual ~Observer() {}// 声明响应更新方法virtual void update() = 0;
};// 具体观察者
class ConcreteObserver : public Observer
{
public:// 实现响应更新方法void update(){// 具体操作}
};// 抽象目标
class Subject
{
public:virtual ~Subject() {}// 添加观察者void attach(Observer *obs){obsList.push_back(obs);}// 移除观察者void detach(Observer *obs){obsList.remove(obs);}// 声明通知方法virtual void notify() = 0;protected:// 观察者列表list<Observer *> obsList;
};// 具体目标
class ConcreteSubject : public Subject
{
public:// 实现通知方法void notify(){// 具体操作// 遍历通知观察者对象for (int i = 0; i < obsList.size(); i++){obsList[i]->update();}}
};// 客户端代码示例
int main()
{Subject *sub = new ConcreteSubject();Observer *obs = new ConcreteObserver();sub->attach(obs);sub->notify();delete sub;delete obs;return 0;
}
#endif
3.观察者模式代码实例
玩过和平精英这款游戏吗?四人组队绝地求生,当一个队友发现物资时,可以发消息“我这里有物资”,其余三个队友听到后可以去取物资;当一个队友遇到危险时,也可以发消息“救救我”,其余三个队友得到消息后便立马赶去营救。本例将用观察者模式来模拟这个过程。
本例的UML图如下:
本例中,抽象观察者是Observer,声明了发现物资或者需要求救时的呼叫的方法call(),具体观察者是Player,即玩家,Player实现了呼叫call()方法,并且还定义了取物资come()和支援队友help()的方法。本例定义了AllyCenter作为抽象目标,它维护了一个玩家列表playerList,并且定义了加入战队和剔除玩家的方法。 具体目标是联盟中心控制器AllyCenterController,它实现了通知notify()方法,该方法将队友call的消息传达给玩家列表里的其余队友,并作出相应的响应。
3.0.公共头文件
通过一个枚举类型来定义两种消息类型,即发现物资和求助
#ifndef __COMMON_H__
#define __COMMON_H__enum INFO_TYPE{NONE,RESOURCE,HELP
};#endif //__COMMON_H__
3.1.观察者
3.1.1.抽象观察者Observer
class Observer {
public:virtual ~Observer() = default;virtual void call(INFO_TYPE, AllyCenter* ac) = 0;const std::string& getName() const { return name; }void setName(std::string n){ name = std::move(n); }protected:std::string name{"none"};
};
3.1.2.具体观察者Player
// final: Player 不能再有子类
class Player final : public Observer {
public:Player() = default;explicit Player(std::string n) { setName(std::move(n)); }void call(INFO_TYPE, AllyCenter* ac) override; // 这里只声明void help() const;void come() const;
};
3.2.目标类
3.2.1.抽象目标AllyCenter
// 前向声明
// 不做前向声明时,两边的头文件往往互相 #include,形成“包含环”。 由于编译是单向读取的
class Observer;
// class Player;// 抽象目标:联盟中心
class AllyCenter {
public:AllyCenter();virtual ~AllyCenter() {}// 声明通知方法virtual void notify(INFO_TYPE infoType, const std::string& name) = 0;// 加入玩家void join(Observer *player);// 移除玩家void remove(Observer *player);protected:// 玩家列表std::vector<Observer*> playerList;
};
3.2.2.具体目标AllyCenterController
// 具体目标
class AllyCenterController : public AllyCenter {
public:AllyCenterController();// 实现通知方法void notify(INFO_TYPE infoType, const std::string& name) override;
};
循环包含
“循环包含”(include cycle)是指:头文件彼此相互 #include(直接或间接 A→B→A)。
预处理展开时会卡在一个环里——虽然 include guard 会阻止“无限展开”,但副作用是某一边在需要完整类型时只看到了前半张脸,于是报“未知类型 / 不完全类型(incomplete type)”之类的编译错误。
错误示例
// A.h
#ifndef A_H
#define A_H
#include "B.h" // A 想用 B
struct A { B b; }; // ← 按值成员,必须要 B 的完整定义
#endif// B.h
#ifndef B_H
#define B_H
#include "A.h" // B 又包含 A
struct B { A a; }; // ← 也按值成员,需要 A 的完整定义
#endif
编译时流程大致是:编译器读 A.h → 进 B.h → 试图再进 A.h,但被 include guard 拦住,于是此时 B.h 里看不到 A 的完整定义,马上报错(反过来顺序也一样)。
“前向声明什么时候不够、必须 #include 对方头?”
核心在于**“不完全类型 vs 完全类型”**。class X; 只是告诉编译器“有个类型 X”,但看不到它的大小、布局和成员。凡是需要知道大小/布局/成员或能析构的场合,都必须拿到完整定义(所以要 #include “X.h”)。
3.3.客户端代码示例及效果
#include <cstdio>
#include <memory>
#include "Observer.h"
#include "AllyCenter.h"int main() {// 创建一个战队auto controller = std::make_unique<AllyCenterController>();// 创建4个玩家,并加入战队auto Jungle = std::make_unique<Player>("Jungle");auto Single = std::make_unique<Player>("Single");auto Jianmengtu = std::make_unique<Player>("Diego");auto SillyDog = std::make_unique<Player>("Richard");controller->join(Jungle.get());controller->join(Single.get());controller->join(Jianmengtu.get());controller->join(SillyDog.get());std::puts("");Jungle->call(RESOURCE, controller.get());std::puts("");SillyDog->call(HELP, controller.get());std::puts("");#ifdef _WIN32system("pause");
#endifreturn 0; // 智能指针自动释放
}
上述代码运行结果如下图:
4.观察者模式的应用
观察者模式是一种使用频率非常高的设计模式,几乎无处不在。凡是涉及一对一、一对多的对象交互场景,都可以使用观察者会模式。比如购物车,浏览商品时,往购物车里添加一件商品,会引起UI多方面的变化(购物车里商品数量、对应商铺的显示、价格的显示等);各种编程语言的GUI事件处理的实现;所有的浏览器事件(mouseover,keypress等)都是使用观察者模式的例子。
5.总结
之后我会持续更新,如果喜欢我的文章,请记得一键三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持!