观察者模式(Observer / Event)是组件协作模式的一种。
动机
- 在软件构建过程中,我们需要为某些对象建立一种“通知依赖关系” ——一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知。如果这样的依赖关系过于紧密,将使软件不能很好地抵御变化。
- 使用面向对象技术,可以将这种依赖关系弱化,并形成一种稳定的依赖关系。从而实现软件体系结构的松耦合。
代码示例:文件分割器
1 class FileSplitter 2 { 3 string m_filePath; 4 int m_fileNumber; 5 ProgressBar* m_progressBar; //通知控件(实现细节),产生了编译时依赖 6 7 public: 8 FileSplitter(const string& filePath, int fileNumber, ProgressBar* progressBar) : 9 m_filePath(filePath), 10 m_fileNumber(fileNumber), 11 m_progressBar(progressBar){ 12 13 } 14 15 void split(){ 16 17 //1.读取大文件 18 19 //2.分批次向小文件中写入 20 for (int i = 0; i < m_fileNumber; i++){ 21 //... 22 float progressValue = m_fileNumber; 23 progressValue = (i + 1) / progressValue; //更新进度条 24 m_progressBar->setValue(progressValue); 25 } 26 27 } 28 };
1 class MainForm : public Form //MainForm主窗口界面 2 { 3 TextBox* txtFilePath; //要分割的文件的路径 4 TextBox* txtFileNumber; //要分割成子文件的个数 5 ProgressBar* progressBar;//分割过程的进度条 6 7 public: 8 void Button1_Click(){ 9 10 string filePath = txtFilePath->getText(); 11 int number = atoi(txtFileNumber->getText().c_str()); 12 //实例化一个文件分割器对象,并初始化 13 FileSplitter splitter(filePath, number, progressBar); 14 15 splitter.split(); 16 17 } 18 };
由于有时文件比较大,新增需求:需要看到文件分割的进度,就在MainForm添加一个子控件processBar
违背了“依赖倒置原则”
在类FileSplitter中,新增的通知控件m_processBar就是实现细节,当需求变更时,实现细节也会变(可能会以百分比显示,省略号显示等)。因此需求变化时,就产生了编译时依赖。
如何重构以上代码?
1 class IProgress{ 2 public: 3 virtual void DoProgress(float value)=0; 4 virtual ~IProgress(){} 5 }; 6 7 8 class FileSplitter 9 { 10 string m_filePath; 11 int m_fileNumber; 12 13 List<IProgress*> m_iprogressList; // 抽象通知机制,支持多个观察者 14 15 public: 16 FileSplitter(const string& filePath, int fileNumber) : 17 m_filePath(filePath), 18 m_fileNumber(fileNumber){ 19 20 } 21 22 23 void split(){ 24 25 //1.读取大文件 26 27 //2.分批次向小文件中写入 28 for (int i = 0; i < m_fileNumber; i++){ 29 //... 30 31 float progressValue = m_fileNumber; // 32 progressValue = (i + 1) / progressValue; 33 onProgress(progressValue);//发送通知 34 } 35 36 } 37 38 39 void addIProgress(IProgress* iprogress){ 40 m_iprogressList.push_back(iprogress); 41 } 42 43 void removeIProgress(IProgress* iprogress){ 44 m_iprogressList.remove(iprogress); 45 } 46 47 48 protected: 49 virtual void onProgress(float value){ 50 51 List<IProgress*>::iterator itor=m_iprogressList.begin(); 52 53 while (itor != m_iprogressList.end() ) 54 (*itor)->DoProgress(value); //更新进度条 55 itor++; 56 } 57 } 58 };
1 //观察者——控件窗口 2 class MainForm : public Form, public IProgress 3 { 4 TextBox* txtFilePath; 5 TextBox* txtFileNumber; 6 7 ProgressBar* progressBar; 8 9 public: 10 void Button1_Click(){ 11 12 string filePath = txtFilePath->getText(); 13 int number = atoi(txtFileNumber->getText().c_str()); 14 15 ConsoleNotifier cn; 16 17 FileSplitter splitter(filePath, number); 18 19 splitter.addIProgress(this); //订阅通知 20 splitter.addIProgress(&cn); //订阅通知 21 22 splitter.split(); 23 24 splitter.removeIProgress(this); 25 26 } 27 28 virtual void DoProgress(float value){ 29 progressBar->setValue(value); 30 } 31 }; 32 33 //观察者——控制台 34 class ConsoleNotifier : public IProgress { 35 public: 36 virtual void DoProgress(float value){ 37 cout << "."; 38 } 39 };
实现细节应依赖于抽象。
progressBar是一个通知控件(实现细节), 本质为通知,故新建一个IProgress类表达一个抽象的通知机制。
让观察者MainForm继承IProgress,
不推荐多继承,但有一种情况: 继承了一个主的继承类,其他都是接口类或抽象基类。
在MainForm中,实现DoProgress方法,即具体细节。FileSpliter不再像之前的代码依赖于界面类,与之解耦合。
模式定义
定义对象间的一种一对多(变化)的依赖关系,以便当一个对象(Subject)的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。 ——《 设计模式》 GoF
类图
Observer -- IProgress
Update() -- doProgress
Subject (可将以下三方法放在基类Subject中实现,让FileSpliter继承此基类)
Attach(Observer ) -- addIProgress()
Detach(Observer ) -- removeIProgress()
Notify() -- onProgress()
ConcreteSubject -- FileSpliter(主体对象)
ConcreteObserver -- 具体的观察者(ConsoleNotifier / MainForm)
要点总结
- 使用面向对象的抽象,Observer模式使得我们可以独立地改变目标与观察者,从而使二者之间的依赖关系达致松耦合。
- 目标发送通知时,无需指定观察者,通知(可以携带通知信息作为参数)会自动传播。
- 观察者自己决定是否需要订阅通知,目标对象对此一无所知。
- Observer模式是基于事件的UI框架中非常常用的设计模式,也是MVC模式的一个重要组成部分。