1、定义与动机
观察者模式定义:定义对象间的一种1对多(变化)的依赖关系,以便当一个对象(Subject)的状态发生比改变时,所有依赖于它的对象都得到通知并且自动更新
- 再软件构建过程中,我们需要为某些对象建立一种“通知依赖关系“——一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知。如果这样的依赖关系过于紧密,僵尸软件不能很好地抵御变化。
- 使用面向对象技术,可以将这种依赖关系弱化,并形成一种稳定的依赖关系。从而实现软件体系结构的松耦合。
2、举例分析
- 例如点击button将一个文件分割成指定数量的功能,在分割过程中一般有一个合理的类似于进度条的通知功能,可以看到当前分割的进度
- 很容易写出下面的代码:
- MainForm在收到触发button按钮的操作之后拿到文件名、需求文件个数就调用FileSplitter进行分割
- FileSplitter类中依赖一个ProgressBar控件,然后在split分割的过程中计算当前分割的进度,将值会写到ProgressBar控件中用于同步展示
- 但是这样写并不好:
- 首先可以思考到的一个问题:ProgressBar写死了,我们只能使用这个控件来实现,如果想换个控件、换个方式或者多个控件来展示似乎不太可行,违背了开闭原则
- 其次很重要的一点:FileSplitter是一个高层模块, ProgressBar是一个低层模块,形成了高层依赖低层模块。而依赖倒置原则讲的是高层和低层模块都应该依赖其抽象,抽象不能依赖实现细节,实现细节应该依赖抽象。ProgressBar实际是一个实现细节(设置value就能展示)
- 而这样的一个依赖会产生问题:如果换个形式进度条展示,需要更改需求时很难做到,难以抵御变化。
class FileSplitter{
private:string filePath;int fileNumber;ProgressBar* progressBar;
public:FileSplitter(string filepath, int number): filePath(filepath), fileNumber(number){}void split(){// 1. 分批次读取文件大小// 2. 分批次向小文件写入for(int i = 0;i < fileNumber;i++){// 处理...// 展示处理的进度条progressBar->setValue(1.0*(i+1) / fileNumber);}}
};class MainForm: public Form{
private:TextBox* txtFilepath;TextBox* txtFileNumber;
public:void button_event_split(){string filePath = txtFilepath->getText();int number = atoi(txtFilepath->getText().c_str());FileSplitter fileSplitter(filePath, number);fileSplitter.split();}
};
3、观察者模式
- 通过分析可知,进度条的展示功能不应该出现在文件分割功能里,应该通知依赖它的地方,进行进度条展示
- 它只需要向外发送通知即可,至于外面进行如何实现这个进度条展示或者其他功能并不需要它关心
- 观察者模式的核心就是向外进行通知,外面收到通知的对象如何实现就由它们自己决定
- 因此观察者模式来做这一需求对上面代码进行改造可以变成如下形式!
3.1、基础优化(一)
class IProgress{
public:virtual void doProgress(double value) = 0;virtual ~IProgress(){}
};class FileSplitter{
private:string filePath;int fileNumber;IProgress *iProgress;
public:FileSplitter(string _filepath, int _number, IProgress *_iProgress): filePath(_filepath), fileNumber(_number), iProgress(_iProgress){}void split(){// 1. 分批次读取文件大小// 2. 分批次向小文件写入for(int i = 0;i < fileNumber;i++){// 处理...// 展示处理的进度条iProgress->doProgress(1.0*(i+1) / fileNumber);}}
};class MainForm: public Form, public IProgress{
private:TextBox* txtFilepath;TextBox* txtFileNumber;ProgressBar *progressBar;
public:void button_event_split(){string filePath = txtFilepath->getText();int number = atoi(txtFilepath->getText().c_str());FileSplitter fileSplitter(filePath, number, this);fileSplitter.split();}virtual void doProgress(double value){progressBar->setValue(value);}
};
3.2、多个观察者(二)
- 当有多个观察者时,可以通过如下的代码来实现
#include <vector>
class IProgress{
public:virtual void doProgress(double value) = 0;virtual ~IProgress(){}
};class FileSplitter{
private:string filePath;int fileNumber;vector<IProgress*> progressList;
public:FileSplitter(string _filepath, int _number, IProgress *_iProgress): filePath(_filepath), fileNumber(_number), iProgress(_iProgress){}void split(){// 1. 分批次读取文件大小// 2. 分批次向小文件写入for(int i = 0;i < fileNumber;i++){// 处理...// 展示处理的进度条notify_observers(1.0*(i+1) / fileNumber);}}void add_IProgress(IProgress* iProgress){progressList.push_back(iProgress);}void remove_IProgress(IProgress* iProgress){progressList.erase(iProgress);}
protected:void notify_observers(double value){for(auto it = progressList.begin();it != progressList.end();it++){(*it)->doProgress(value)}}
};class MainForm: public Form, public IProgress{
private:TextBox* txtFilepath;TextBox* txtFileNumber;ProgressBar *progressBar;
public:void button_event_split(){string filePath = txtFilepath->getText();int number = atoi(txtFilepath->getText().c_str());FileSplitter fileSplitter(filePath, number, this);// 添加额外的观察者fileSplitter.add_IProgress(...);fileSplitter.split();}virtual void doProgress(double value){progressBar->setValue(value);}
};class ConsoleNotifier: public IProgress{virtual void doProgress(double value){//...具体实现...}
};
4、总结
- 上面的举例代码中Subject对应FileSplitter、而下面的ConcreteSubject就是那个Vector容器以及加和删除的方法。
- Observer对应上面的进度条类型
- ConcreteObserver对应MainForm
- 使用面向对象的抽象,Observer模式使得我们可以独立地改变目标与观察者,从而使二者之间的依赖关系达到松耦合
- 目标发送通知时,无需指定观察者,通知(可以携带通知信息做为参数)会自动传播
- 观察者自己决定是否需要订阅通知,目标对象对此一无所知
- Observer模式是基于事件的UI框架中非常常用的设计模式,也是MVC模式的重要组成部分