Observer 观察者模式
组件协作模式通过晚绑定,来实现框架与应用程序之间的松耦合。是框架和引用程序协作常用的。
动机
我们需要为一些对象建立一种“通知依赖”关系,即一个对象状态发生变化,所有的依赖对象(观察者对象)都将得到通知,如果依赖过于紧密,软件不能很好地抵御变化。面向对象技术可以使这种依赖关系弱化,并形成一种稳定的依赖关系,从而体系结构会松耦合。
定义
对象间一种一对多(变化)的依赖关系,以便当一个对象的状态(目标对象)发生变化时,所有依赖它的对象都得到通知并自动更新。
示例代码
class FilterSplitter {
private:
string m_filePath;
int m_fileNumber;
public:
FilterSplitter(cons string& filePath, int fileNumber) :
m_filePath(filePath), m_fileNumber(fileNumber) {}
void split() {
// 1. 读取文件
// 2. 分批次向小文件写入
for(int i = 0; i< m_fileNumber; ++i) {
//...
}
}
};
class MainForm : public Form {
private:
TextBox* txtFilePath;
TextBox* txtFileNumber;
public:
void Button1_Click() {
string filePath = txtFilePath->getText();
int number = atoi(txtFileNumber->getText().c_str());
FilterSplitter splitter(filePath, number);
splitter.split();
}
};
假设现在有这样一个文件分割功能的代码,其中 FilterSplitter 类主要负责文件的切割,MainForm 则是直接触发文件分割的类。现在我们需求发生变化,想要在分割文件的同时,可以将分割进度条展现出来,有一个新的类 ProgressBar 可做这个事。可能比较常规的一种做法是,将 ProgressBar 作为参数传入 FilterSplitter 类中,方便调用其成员函数。
class FilterSplitter {
private:
string m_filePath;
int m_fileNumber;
ProgressBar * m_progressBar; // 新增代码
public:
FilterSplitter(cons string& filePath, int fileNumber, ProgressBar* progressBar) :
m_filePath(filePath), m_fileNumber(fileNumber), m_progressBar(progressBar) {} // 新增代码
void split() {
// 1. 读取文件
// 2. 分批次向小文件写入
for(int i = 0; i< m_fileNumber; ++i) {
//...
// 新增代码
if (m_progress != nullptr) {
float progressValue = m_fileNumber;
progressValue = (i+1) / progressValue;
m_progressBar->setValue(progressValue); // 更新并显示进度条
}
}
}
};
class MainForm : public Form {
private:
TextBox* txtFilePath;
TextBox* txtFileNumber;
ProgressBar *progressBar; // 新增代码
public:
void Button1_Click() {
string filePath = txtFilePath->getText();
int number = atoi(txtFileNumber->getText().c_str());
FilterSplitter splitter(filePath, number, progressBar); // 新增代码
splitter.split();
}
};
可以看到我们分别在 MainForm 界面类和 FilterSplitter 文件分割类中引入了新的界面相关的进度条类 ProgressBar,虽然这样可以满足我们的功能要求,但是这样就依赖了一个具体的进度条类 ProgressBar,试想一下,现在需求又变了,我们需要用更换另一种数字显式进度的方式,那么我们就需要修改 FilterSplitter 涉及到 ProgressBar 部分的源码。
首先这是违背了依赖倒置原则的,即依赖抽象而不是细节,ProgressBar 对我们来说就是细节,其次有需求变更就要修改源码也违背了开闭原则。如果想着去找通知进度相关的基类,但是其进度更新子类的接口可能是不统一的,ProgressBar 是 setValue,其他的子类可能自己实现的是 updateValue 接口。所以对于代码优化单纯的去找父类去实现依赖抽象,有的时候也未必合适。
这里 ProgressBar 主要是起到了一个通知的作用,更好地方式是可以进一步将这个功能抽象成一个基类,这个接口基类统一了更新数值的接口,并且有界面类 MainForm 继承接口并实现它,然后对 FilterSplitter 类进行修改,通过依赖抽象基类来松耦合。
class IProgress { // 接口基类
public:
virtual void DoProgress(float value) = 0;
~IProgress(){}
};
class FilterSplitter {
private:
string m_filePath;
int m_fileNumber;
// ProgressBar * m_progressBar; // 依赖具体
IProgress * m_progress; // 抽象机制
public:
FilterSplitter(cons string& filePath, int fileNumber, IProgress* iProgress) :
m_filePath(filePath), m_fileNumber(fileNumber), m_progress(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 progressValue) {
if (m_progress != nullptr) {
m_progress->DoProgress(progressValue); // 通知更新进度
}
}
};
class MainForm : public Form, public IProgress{ // 这里是多继承
private:
TextBox* txtFilePath;
TextBox* txtFileNumber;
ProgressBar *progressBar; // 新增代码
public:
void Button1_Click() {
string filePath = txtFilePath->getText();
int number = atoi(txtFileNumber->getText().c_str());
// 因为是多继承,可以直接传 MainForm 类,这里就是 this 指针
FilterSplitter splitter(filePath, number, this);
splitter.split();
}
// 在 FilterSplitter 中通过 this 指针,真正调用的就是这里的实现
void DoPregress(float value) {
progressBar->setValue(value);
}
};
这个只是单个对象的通知,同样还可以通过多个对象的通知,进一步提供相关增删的接口即可:
class IProgress { // 接口基类
public:
virtual void DoProgress(float value) = 0;
~IProgress(){}
};
class FilterSplitter {
private:
string m_filePath;
int m_fileNumber;
List<IProgress *> m_progressList; // 抽象机制,支持多个观察者
public:
FilterSplitter(cons string& filePath, int fileNumber) :
m_filePath(filePath), 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);
}
}
// 增加观察者
void addIProgress(IProgress* iprogress) {
m_progressList.push_back(iprogress);
}
// 消除观察者
void removeIProgress(IProgress* iprogress) {
m_progressList.remove(iprogress);
}
protected:
void onProgress(float progressValue) {
// 遍历通知更新进度
List<IProgress *>::iterator itor = m_progressList.begin();
for(;itor!=m_progressList.end();++itor) {
(*itor)->DoProgress(progressValue);
}
}
};
// 新增通知方式
class OtherNotifier : public IProgress {
public:
void DoProgress(float value) {
cout << "#";
}
}
class MainForm : public Form, public IProgress{
private:
TextBox* txtFilePath;
TextBox* txtFileNumber;
ProgressBar *progressBar; // 新增代码
public:
void Button1_Click() {
string filePath = txtFilePath->getText();
int number = atoi(txtFileNumber->getText().c_str());
FilterSplitter splitter(filePath, number); // 因为是多继承,可以直接传 MainForm 类
OtherNotifier otherNotifier;
splitter.addIProgress(this); // 订阅通知
splitter.addIProgress(&otherNotifier); // 订阅通知
splitter.split();
splitter.removeIProgress(this); // 取消订阅
}
void DoPregress(float value) {
progressBar->setValue(value);
}
};
以上就是通过接口抽象基类,以及多继承完成的观察者模式示例代码。
结构图
总结
- Observer 观察者模式使我们可以独立地改变目标与观察者,从而使二者达到松耦合。
- 目标发出通知时,无需指定观察者,通知会自动传播
- 观察者自己决定是否需要订阅通知,目标对象对此一无所知
其他设计模式汇总:
[设计模式] —— 设计模式的介绍及分类