观察者模式也是一种"组件协作"模式
动机
- 在软件构建过程中,我们需要为某些对象建立一种"通知依赖关系"。一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知。如果这样的依赖关系过于紧密,将使软件不能很好的抵御变化。
使用面向对象技术,可以将这种依赖关系弱化,并形成一种稳定的依赖关系。从而实现软件体系结构的松耦合。
举个栗子
我们有一个将大文件分割成指定文件个数的任务。
cpp
class FileSplitter{
private:
string m_filePath;
int m_fileNumber;
public:
FileSplitter(const string& filePath, int fileNumber) :
m_filePath(filePath), m_fileNumber(fileNumber){}
void split(){
//读取filePath文件
//分批次向fileNumber个文件中写入
for (int i = 0; i < m_fileNumber; i++){
//写入文件
}
}
};
cpp
class MainForm : public Form{
private:
TextBox* txtFilePath;
TextBox* txtFileNumber;
public:
void Button_Click(){
string filePath = txtFilePath->getText();
int number = atoi(txtFileNumber->getText().c_str());
FileSplitter splitter(filePath, number);
splitter.split();
}
};
以上是实现这个基本文件分割需求的两个类的伪代码。分为实现文件分割的代码,和一个窗口按钮事件的代码。
现在我们需要在界面上显示其进度。这是一个经典的需要观察的场景,界面需要获取分割的实时进度,并且进行相应的处理。
cpp
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(){
//读取filePath文件
//分批次向fileNumber个文件中写入
for (int i = 0; i < m_fileNumber; i++){
//写入文件
if(m_progressBar!=nullptr)
m_progressBar->setValue( (i+1) / m_fileNumber); //更新进度条
}
}
};
cpp
class MainForm : public Form{
private:
TextBox* txtFilePath;
TextBox* txtFileNumber;
ProgressBar* progressBar;
public:
void Button_Click(){
string filePath = txtFilePath->getText();
int number = atoi(txtFileNumber->getText().c_str());
FileSplitter splitter(filePath, number, progressBar);
splitter.split();
}
};
我们可能会想到在分割时获取分割进度,并且在界面中显示的方法。
存在的问题
这样的作法其实违背了依赖倒置的设计原则。
什么是依赖
我们这里讨论一下什么是依赖------如果A依赖B,那么A编译的时候需要B存在才能编译通过。也就是编译时依赖。
我们再看上面的代码实现,修改后的FileSplitter 类依赖于组件类ProgressBar 的实现。我们思考这样的情况,如果需求再次发生变更,从显示进度条修改成显示进度的百分比。那么我们就需要再次修改FileSplitter类的内容,换一个组件来实现这一功能。
这样的依赖是一个不好的依赖,因为实现的细节会经常发生改变。
观察者模式
重构代码
cpp
class IProgress{ //I表示这个类是一个接口,也就是表示这个类是一个抽象基类
public:
virtual void DoProgress(float value) = 0;
virtual ~IProgress(){}
};
class FileSplitter{
private:
string m_filePath;
int m_fileNumber;
//ProgressBar* m_progressBar; 这是一个通知控件
IProgress* m_iprogress; //抽象的通知机制
public:
FileSplitter(const string& filePath, int fileNumber, IProgress* iprogress) :
m_filePath(filePath), m_fileNumber(fileNumber),m_iprogress(iprogressb){
//...
}
void split(){
//读取filePath文件
//分批次向fileNumber个文件中写入
for (int i = 0; i < m_fileNumber; i++){
//写入文件
if(m_iprogress!=nullptr)
m_iprogress->DoProgress( (i+1) / m_fileNumber); //更新进度条
}
}
};
cpp
class MainForm : public Form, public IProgress{
private:
TextBox* txtFilePath;
TextBox* txtFileNumber;
ProgressBar* progressBar;
public:
void Button_Click(){
string filePath = txtFilePath->getText();
int number = atoi(txtFileNumber->getText().c_str());
FileSplitter splitter(filePath, number, this);
splitter.split();
}
void DoProgress(float value){
progressBar->setValue(value);
}
};
如果我们使用单纯的使用找基类的方法来重构,我们可能发现,ProgressBar这个类的基类,很有可能根本没有setValue这个方法。
我们观察上面重构后的代码,我们将**"通知"** 这一行为进行了抽象,不再使用通知组件这一个具体的对象来完成,而是通过一个抽象的虚的基类IProgress来实现。彻底将分割文件这个类与具体的组件类解耦。
我们让MainForm 类进行一个多重继承,继承了IProgress的接口。再在其中进行DoProgress这一通知行为。(MainForm类是窗口的实现类,与具体的组件类紧耦合是正常的)
C++中尽量不要使用多重继承,但是推荐使用一种多重继承,就是一个是主继承类,其他都是接口。
进一步重构代码
目前我们实现了一个观察者的情况,如果需要有多个观察者的时候,需要进一步完善我们的机制。
cpp
class IProgress{ //I表示这个类是一个接口,也就是表示这个类是一个抽象基类
public:
virtual void DoProgress(float value) = 0;
virtual ~IProgress(){}
};
class FileSplitter{
private:
string m_filePath;
int m_fileNumber;
vector<IProgress*> m_iprogressList; //多个观察者
public:
FileSplitter(const string& filePath, int fileNumber) :
m_filePath(filePath), m_fileNumber(fileNumber){
//...
}
void add_IProgress(IProgress* iprogress){
m_iprogressList.push_back(iprogress);
}
virtual void remove_IProgress(IProgress* iprogress){
m_iprogressList.remove(iprogress);
}
void split(){
//读取filePath文件
//分批次向fileNumber个文件中写入
for (int i = 0; i < m_fileNumber; i++){
//写入文件
float progressValue = (i + 1) / m_fileNumber;
onProgress(progressValue );
}
}
protected:
void onProgress(float value){
for(auto item = m_iprogressList.begin(); item != m_iprogressList.end(); item++){
item->DoProgress(value);
}
}
};
可以将通知这一行为单独作为一个受保护的虚函数来实现,这也是现在一些框架的做法。
相应的MainForm类中,
cpp
FileSplitter splitter(filePath, number);
splitter.add_IProgress(this);
splitter.add_IProgress(.....);
模式定义
定义对象间的一种一对多(变化)的依赖关系,以便当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并更新。 ------《设计模式》GoF
结构
类图中稳定的部分是红色的,蓝色部分是变化的。
对应上面的例子中的代码。
Oberver -> IProgress Update() -> DoProgress()
ConcreteObserver -> MainForm/ProgressBar Update() -> 重写的DoProgress()
Subject : Attach()->addProgress() Detach()->removeProgress() Notify()->onProgress()这三个方法可以单独写成一个虚基类,也可以像上面代码中一样实现
ConcreteSubject -> FileSplitter
总结
- 使用面向对象的抽象,观察者模式可以独立地改变目标与观察者,达到松耦合
- 目标发送通知时,无需指定观察者。通知可以携带信息自动传播。
- 观察者自己决定是否需要订阅通知,目标对象对此一无所知。
- 观察者模式是基于事件的UI框架中非常常用的模式。