接上篇,本篇将会介绍C++设计模式中的Observer 观察者模式,和前2篇模板方法Template Method
及Strategy 策略模式
一样,仍属于“组件协作”模式。Observer 在某些领域也叫做 Event
。
“通知依赖关系”
——一个对象(目标对象
)的状态发生改变,所有的依赖对象(观察者对象
)都将得到通知。如果这样的依赖关系过于紧密,将使软件不能很好地抵御变化。假设以下的场景需求:做一个文件的分割器。虽然现在文件分割器使用的比较少,但是在之前是使用很广泛的,因为当时还是一个3寸盘的时代,经常需要将大文件拷走,就需要将大的文件分隔为多个文件拷贝携带走。
下面是一个伪码,只会展示主干部分。
首先需要一个界面,mainform就是一个windows界面,父类为Form,主要有两个控件txtFilePath(大文件的全路径)和txtFileNumber(希望分割的文件个数)(此处给出的是后期修改后的代码)。
Button1_Click
函数中收集用户输入的2个参数信息,传递给FileSplitter splitter,splitter调用split()
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();
}
};
FileSplitter中放文件变量,m_filePath
负责文件路径,m_fileNumber
负责文件个数,通过构造器给这些成员变量赋值。
class FileSplitter
{
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);
}
}
};
上面代码就实现了在Button1_Click
时的文件的分割功能。
假设有一个用户需求,希望进行文件分割时,如果文件特别大,需要分割很长时间,这个时候需要提供一个进度条,进行进度展示。
最朴素的想法就是在界面中提供一个ProgressBar* progressBar
,并且在FileSplitter
中提供方法,代码结果如上。
但是上面的实现方式是否违背了某一设计原则呢?即违背依赖倒置原则(DIP)
上面实现中,一般我们所讲的依赖就是编译式依赖
(除非明确提出运行式依赖),例如A依赖B也就是A编译时B必须存
在FileSplitter中
的ProgressBar* progressBar
就产生了编译式依赖,这个ProgressBar* progressBar
就是依赖倒置原则(DIP)中讲到的实现细节(为什么说它是实现细节呢?因为进度的显示方式可能会有变化,这就带了实现细节层面变更的困扰)
需要对上面的代码进行重构分析,可以看到ProgressBar* progressBar
实际扮演的是一个通知
,可以使用一种抽象的方式来表达一个通知而不需要具体的控件表达通知。
class IProgress{
public:
virtual void DoProgress(float value)=0;
virtual ~IProgress(){}
};
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++;
}
}
};
C++中支持多继承,一般不推荐使用多继承的方式,可能会导致复杂的耦合性问题,但是C++推荐一种多继承的形式就是一个是主继承类(如public Form),其他是接口或者抽象基类(public IProgress)。
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());
ConsoleNotifier cn;
FileSplitter splitter(filePath, number);
splitter.addIProgress(this); //订阅通知
splitter.addIProgress(&cn); //订阅通知
splitter.split();
splitter.removeIProgress(this);
}
virtual void DoProgress(float value){
progressBar->setValue(value);
}
};
class ConsoleNotifier : public IProgress {
public:
virtual void DoProgress(float value){
cout << ".";
}
};
定义对象间的一种一对多
(变化)的依赖关系,以便当一个对象(Subject)的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。 ——《设计模式》 GoF
上图是《设计模式》GoF中定义的Observer 观察者模式的设计结构。结合上面的代码看图中最重要的是看其中稳定和变化部分,也就是下图中红框和蓝框框选的部分。
GoF的设计模式中建议将addIProgress,removeIProgress,onProgress放到父类中,让FileSplitter去继承父类。而此处我们的框架是将其直接写到FileSplitter中,不管是否提出Subject都是观察者模式,本博文重构的代码就没有提出Subject,其实是将Subject和ConcreteSubject合二为一。
独立地改变
目标与观察者,从而使二者之间的依赖关系达致松耦合。代码中随便添加观察者,但是addIProgress,removeIProgress,onProgress保持复用性不变
onProgress(progressValue);//发送通知
不知道谁是观察者,针对通知机制抽象通知
splitter.addIProgress(this); //订阅通知splitter.addIProgress(&cn); //订阅通知
Observer 观察者模式与模板方法Template Method是一样的常用。例如java中的listener就是观察者模式、C#中的Event也是观察者模式
Observer模式需要多思考,最关键的是抽象的通知依赖关系。