观察者模式 Observer
观察者模式 Observer
极简一句话:报社包含一个订阅读者表(这个表里的人必须继承读者或有取书的方法)
所属分类——“组件协作” 模式
动机(Motivation)
简概
- 在软件构建过程中,我们需要为某些对象建立一种 “通知依赖关系” ——一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知。如果这样的依赖关系过于紧密,将使软件不能很好地抵御变化
- 使用面向对象技术,可以将这种依赖关系弱化,并形成一种稳定的依赖关系。从而实现软件体系结构的松耦合
核心:1 让通知者可以通知多个观察者,2 观察关系可动态绑定
消息通知时,通知者和被通知者两个对象解耦、可以动态绑定
不用这个设计模式也能实现消息通知,子父对象粗暴地互相包含对方指针,然后顺着指针一层层找到要通知的对象并调用其方法即可
但这种方法高度耦合,而且做不到一点 —— 能够动态地组合消息通知关系
——
什么时候不用?仅A类和B类两者之间有通知关系的需求
什么时候用?被通知类,有可能被各种东西进行通知
代码体现
文件分割器 + 进度条/命令行显示进度的实现
考虑时间轴:后续加入增加进度条的功能,再后续可能又要要求这个进度条显示百分比,再后续又有可能要在命令行而不是在GUI中显示
举例 - 写法1(高耦合普通写法)
该写法中,库类与界面类耦合了,库类依赖了实现细节m_progressBar、违背了依赖倒置原则(DIP)
(这里的ProgressBar属于运行实现细节,他编译时依赖于FileSplitter)
文件分割器类
/** 传入包含
*/
class Filesplitter // 文件分割器(把大文件分割成多个小文件方便拷贝)
{
string m_filePath;
int m_fileNumber;
ProgressBar* m_progressBar; // 【新增】声明进度条,是个通知控件
public:
FileSplitter(const string& filePath, int fileNumber
,ProgressBar* progressBar // 【新增】传入进度条
):m_filePath(filePath),
m_progressBar(progressBar), // 【新增】初始化进度条
m_fileNumber(fileNumber){// 传入文件和文件分割数量
}
void split(){
// 1.读取大文件
// 2.分批次向小文件中写入
for (int i - 0; i < m_fileNumber; i++){
//...
if(m_progressBar != nullptr){ // 【新增】更新进度条
float progressValue = m_fileNumber;
progressValue = (i+1)/progressValue;
m_progressBar->setValue((i+1)/m_fileNumber);
}
}
}
};
运行代码
/** 创建包含
*/
class MainForm : public Form
{
TextBox* txtFilePath;
TextBox* txtFileNumber;
ProgressBar* progressBar; // 【新增】声明进度条
public:
void Button1 click(){
string filePath = txtFilePath->getText();
int number = atoi(txtFileNumber->getText().c_str());
FileSplitter splitter(filePath,number // 文件分割器类
,progressBar // 【新增】传入进度条
);
splitter.split(); // 调用分割方法
}
};
举例 - 写法2(观察者写法)
单纯找基类不行
文件分割器类(含有观察者)
class IProgress{ // 【新增】通知机制的抽象基类
public:
virtual void DoProgress(float value)=0;
virtual ~IProgress(){}
}
class Filesplitter // 文件分割器(把大文件分割成多个小文件方便拷贝) { string m_filePath; int m_fileNumber; //ProgressBar* m_progressBar; IProgress* m_iprogress; // 【新增】抽象通知机制 public: FileSplitter(const string& filePath, int fileNumber ,IProgressBar* iprogressBar // 【新增】传入通知机制 ): m_filePath(filePath), m_progress(iprogress), // 【新增】初始化通知机制 m_fileNumber(fileNumber){// 传入文件和文件分割数量
}
void split(){
// 1.读取大文件
// 2.分批次向小文件中写入
for (int i - 0; i < m_fileNumber; i++){
//...
float progressValue = m_fileNumber; // 【新增】
progressValue = (i+1)/progressValue;
onProgress(progressValue);
}
}
protected: void onProgress(float value){ // 【新增】更新通知机制 if(m_iprogress != nullptr){ m_iprogress->DoProgress(value); } } };
运行代码
```c++
class MainForm : public Form // 【观察者】
, public IProgress // 【新增】多继承一个抽象基类接口
{
TextBox* txtFilePath;
TextBox* txtFileNumber;
ProgressBar* progressBar; // 【新增】声明进度条
public:
void Button1 click(){
string filePath = txtFilePath->getText();
int number = atoi(txtFileNumber->getText().c_str());
FileSplitter splitter(filePath, number // 文件分割器类
,this // 【新增】传入通知机制(this继承了IProgress)
);
splitter.split(); // 调用分割方法
}
virtual void DoProgress(float value) override{ // 【新增】重写抽象基类接口
progressBar-<>setValue(value);
}
};
举例 - 写法3(多个观察者)
上面是观察了一个,改变一个。但实际上观察者模式可能会改变多个
文件分割器类(支持多个观察者)
class IProgress{ // 通知机制的抽象基类
public:
virtual void DoProgress(float value)=0;
virtual ~IProgress(){}
}
class Filesplitte
{
string m_filePath;
int m_fileNumber;
List<IProgress*> m_iprogressList; /* 【修改】变为通知机制的vector容器,支持了多个观察者
* 或者用Vector等其他容器也是可以的*/
public:
FileSplitter(const string& filePath, int fileNumber
):m_filePath(filePath),
m_fileNumber(fileNumber){// 传入文件和文件分割数量
}
void add_IProgress(IProgress* iprogress){ // 【新增】将通知进制放入vector容器中
//m_iprogressVector.push_back(iprogress);
m_iprogressList.add(iprogress);
}
void remove_IProgress(IProgress* iprogress){ // 【新增】将通知进制从vector容器中删除
//m_iprogressVector.remove(iprogress);
m_iprogressList.remove(iprogress);
}
void split(){
// 1.读取大文件
// 2.分批次向小文件中写入
for (int i - 0; i < m_fileNumber; i++){
//...
float progressValue = m_fileNumber;
progressValue = (i+1)/progressValue;
onProgress(progressValue);
}
}
protected:
void onProgress(float value){
List<IProgress*>::Iterator itor=m_iprogressList.begin(); // 【修改】用迭代器遍历
while(itor!=m_iprogressList.end())
{
(*itor)->DoProgress(value);// 循环调用每个Iterator的DoProgress方法(更新进度条)
itor++;
}
}
};
运行代码
class MainForm : public Form // 继承IProgress,所以this也是 “观察者”
, public IProgress
{
TextBox* txtFilePath;
TextBox* txtFileNumber;
ProgressBar* progressBar; // 【新增】声明进度条
public:
void Button1 click(){
string filePath = txtFilePath->getText();
int number = atoi(txtFileNumber->getText().c_str());
ConsoleNotifier cn;
FileSplitter splitter(filePath,number); // 文件分割器类
splitter.addIprogress(this); // 》》这里就添加了两个观察者(实现代码决定:是否订阅通知)
splitter.addIprogress(&cn); // 》》这里就添加了两个观察者(实现代码决定:是否订阅通知)
splitter.split(); // 调用分割方法
}
virtual void DoProgress(float value) override{ // 【新增】重写抽象基类接口
progressBar-<>setValue(value);
}
};
class ConsoleNotifier : public IProgress // 继承IProgress,该类成为了 “观察者”
{
public:
virtual void DoProgress(float value){
cout << "."
}
}
比较两种写法
- 写法一:分析存在什么问题时就要想有没有违背八大设计原则 该写法中,库类与界面类耦合了,库类依赖了实现细节m_progressBar、违背了
依赖倒置原则(DIP)
(这里的ProgressBar属于运行实现细节,他编译时依赖于FileSplitter) - 写法二/三:文件分割器类中定义IProgress(通知机制)而不是ProgressBar(进度条) 没有耦合界面类,把紧耦合变成松耦合,遵循了
依赖倒置原则(DIP)
设计模式
模式定义
定义对象间的一种一对多(变化)的依赖关系,以便当一个对象(Subject)的状态发生改变时,所有依赖于它的对象都得到通知并自动更新
——《设计模式》GoF
结构(Structure)
新
原写法
或
改进后
概括变化
其实就是让 通知类 中,所包含的指针,由具体类变成抽象类 如此一来 通知类,就能通知更多的观察者类
在这里被通知者是MainForm
旧
旧Mermaid
旧Mermaid(红色表示稳定,类图左观察右,又继承左)
结合程序
- Observer(观察者)相当于例程中的IProgress(通知机制),Update()相当于DoProgress()
- ConcreteObserver(具体观察者)相等于例程中的MainForm和ConsoleNotifier
- Subject相当于Filesplitte,里面Attach、Detach、Notify分别相当于例程的add_IProgress、remove_IProgress、onProgress
- 例程中的ConcreteSubject和Subject在例程中都是Filesplitte,例程为了方便显示合二为一了,但开发中可以分开
和Template Method一样都是非常常见的设计模式
其他语言和架构:
- Java中的Listener机制
- C#的Event模式
- Qt的single-slot机制、Model-View模式
- Vue的核心——数据驱动视图(Observer模块)
要点总结
- 使用面向对象的抽象,Observer模式使得我们可以独立地改变目标与观察者,从而使二者之间的依赖关系达致松耦合
- 目标发送通知时,无需指定观察者,通知(可以携带通知信息作为参数)会自动传播
- 观察者自己决定是否需要订阅通知,目标对象对此一无所知
- Observer模式是基于事件的UI框架中非常常用的设计模式,也是MVC模式的一个重要组成部分
个人体会
结合QT
很像是QT的信号和槽的机制,在ConcreteObserver中定义定义槽,ConcreteSubject中定义信号、并连接信号和槽来决定观察者是否订阅通知
Model-View模式应该也是Observer模式的原理
观察者模式又叫做
- 发布-订阅(Publish/Subscribe)模式
- 模型-视图(Model/View)模式
- 源-监听器(Source/Listener)模式
- 从属者(Dependents)模式
一对多
说是一对多,但事实上应该是可以多对多的吧
- 被观察者是一(single)、观察者是多(slot)
- 被观察者通知观察者、观察者订阅被观察者
- 观察者主动决定去观察被观察者