观察者模式
动机
在软件构建过程中,我们需要为某些对象建立一种“通知依赖关系”——一个对象的状态改变,所有的依赖对象(观察者对象)都能将得到通知.如果这样的依赖关系过于紧密,将使软件不能很好的抵御变化。
使用面向对象的技术,将这种依赖关系弱化,形成稳定的依赖关系,实现松耦合。
模式定义
定义一种对象之间的一对多的依赖关系,以便当一个对象的状态改变时,所有依赖于它的对象都得到通知并自动更新。
——《设计模式》GoF
例子
要实现一个文件分割的功能,就是将一个大的文件分割为几个小的文件;
在窗口处理程序中,点击Button1将会创建splitter
对象,并调用分割程序split
;
MainForm还有一个显示当前进度的功能,在类中有ProgressBar*
,并将其传入Splitter对象,Splitter对象根据自己进度调用progressbar
来显示进度。
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());
FileSplitter splitter(filePath, number, progressBar);
splitter.split();
}
};
在FileSplitter
对象中,根据传入的参数调用构造函数,在此类中还必须有一个与MainFrom
对象对应的ProgressBar
类型的对象。
class FileSplitter
{
private:
string m_filePath;
int m_fileNumber;
ProgressBar* m_progressBar;
public:
FileSplitter(const 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++){
//...
float progressValue = m_fileNumber;
progressValue = (i + 1) / progressValue;
m_progressBar->setValue(progressValue);
}
}
};
然后根据分割的进度调用ProgressBar
对象。
那么问题是什么呢?
问题就是MainFrom
中若要更改显示进度的方式,不在使用ProgressBar
类型对象,或者添加一种显示进程的方式,那么FileSplitter
中也必须更改内容,出现了紧耦合。
改进版
首先将调用进度条显示,声明基类纯虚函数,DoProgress就是进程显示函数
class IProgress{
public:
virtual void DoProgress(float value)=0;
virtual ~IProgress(){}
};
下面是两种进度显示的方式,第一种类似于控制台应用程序,第二种就是Windows自己的的进度条对象
//类似于控制台应用程序打点显示进程
class ConsoleNotifier : public IProgress {
public:
virtual void DoProgress(float value){
cout << ".";
}
};
//进度条显示
class Probar : public IProgress {
public:
ProgressBar* progressBar;
virtual void DoProgress(float value){
progressBar->setValue(value);
}
};
MainFrom就更改为:
class MainForm : public Form, public IProgress
{
private:
TextBox* txtFilePath;
TextBox* txtFileNumber;
public:
void Button1_Click(){
string filePath = txtFilePath->getText();
int number = atoi(txtFileNumber->getText().c_str());
ProgressBar* progressBar;//一种进度显示对象
ConsoleNotifier cn;//另一种进度显示对象
FileSplitter splitter(filePath, number);//构造文件分割对象
splitter.addIProgress(this); //将进度显示方式添加到该对象
splitter.addIProgress(&cn); //将进度显示方式添加到该对象
splitter.split();//开始分割
splitter.removeIProgress(this);
};
注意这里addIProgress
是因为splitter
类用一个list
存储多个进度显示对象。
文件分割类的内容如下:
class FileSplitter
{
string m_filePath;
int m_fileNumber;
List<IProgress*> m_iprogressList; // 抽象通知机制,支持多个观察者
public:
FileSplitter(const 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_iprogressList.push_back(iprogress);
}
void removeIProgress(IProgress* iprogress){//删除进度显示方式
m_iprogressList.remove(iprogress);
}
protected:
virtual void onProgress(float value){
List<IProgress*>::iterator itor=m_iprogressList.begin();
while (itor != m_iprogressList.end() )
(*itor)->DoProgress(value); //更新进度条
itor++;
}
}
};
Splitter对象是根据已经分割成小文件的数量显示进度的,当调用
addIProgress(IProgress* iprogress)
时,向List<IProgress*> m_iprogressList
添加进度显示方式,而且是基类指针指向子类对象,所以可以多态调用进度显示。
文件分割时通过 onProgress(progressValue)
依次更新进度条。
通过以上方式,进度显示将实现松耦合,根据需要往Splitter
内添加进度显示方式。当然也可以不使用list
这种方式。
总结
-
使用面向对象的抽象,Observer模式使得我们可以独立的改变目标(
FileSplitter)
和观察者(MainFrom
),从而使二者的依赖关系松耦合。 -
目标发送通知,无需指定观察者,通知会自动传播。
-
观察者可以自己决定是否需要订阅通知(MainFrom中addProgress),目标对象对此一无所知(FileSplitter只根据LIST的基类指针依次调用)。
-
观察者模式是基于事件的UI框架中非常常用的设计模式。