属于类别
- 组件协作模式
动机
在软件构建过程中,我们需要为某些对象建立一种“通知依赖关系”——一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都得到通知。如果这样的依赖关系过于紧密,将使软件不能很好地抵御变化。
使用面向对象技术,可以将这种依赖关系弱化,并形成一种稳定的依赖关系。从而实现软件体系结构的松耦合。
代码
文件分割器
需求:实现一个文件分割器
//MainForm.cppclass MainForm : public Form{TextBox* txtFilePath; //文件路径TextBox* txtFileNumber; //分割的个数public:void Button1_Click() {string filePath = txtFilePath->getText();int number = atoi(txtFileNumber->getText().c_str());FileSplitter splitter(filePath, number);splitter.split();}}//FileSplitter.cppclass FileSplitter{string m_filePath;int m_fileNumber;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) {//...}}}
提供一个进度条
//mainform.cppclass 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.cppclass 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++){//...//进度条展示if (m_progressBar != nullptr) {float progressValue = m_fileNumber;progressValue = (i + 1) / progressValue;m_progressBar->setValue(progressValue);}}}};
抽象出基类
上面这样写有什么问题?
- 不要想有什么设计模式, 而是想这样做的话会违反八大设计原则中的哪些条款
- 违反了第一条设计原则,依赖倒置设计原则
依赖倒置原则:
- 高层模块不能依赖底层模块,二者都应该依赖于抽象
- 抽象不能依赖实现细节,实现细节应该依赖抽象
什么是依赖?
- A依赖B,A编译的时候,B需要存在,才能编译通过
- 我们在设计模式中所说的依赖,默认都是编译式依赖,除非明确提出是运行式依赖
那这个案例中产生了什么不良依赖?
FileSplitter依赖于ProgressBar- 而
FileSplitter中的ProgressBar就是实现细节- 目前展示进度用的是
ProgressBar - 但是不能确定明天使用的还是
ProgressBar - 有可能需求变了,明天改成了用label,直接以百分比的形式展现
- 也有可能这个程序明天要跨平台,是一个控制台程序,没有窗口。我希望在控制台上打一个一个点,来展示它的进度
- 目前展示进度用的是
- 所以当需求变更时,ProgressBar这个实现细节就会给我们带来困扰。为什么不喜欢依赖实现细节呢?因为实现细节非常容易变
那怎么样去解决这个问题,怎么样重构这个代码呢?
答:依赖倒置原则的解决方案。你不要去依赖A,而是去依赖A的抽象基类
FileSplitter不要依赖ProgressBar,而是去依赖ProgressBar的基类ProgressBar的基类是什么呢?可能是ControlBase这样一个基类。但是单纯找ProgressBar的父类的话,你会发现你自己走入了一个死胡同。ControlBase可没有setValue的方法,甚至没有更新界面进度条的相关方法。- 所以单纯找基类可能是一个很粗浅的方法。我们应该深入的思考,我们具体应该要解决什么问题。ProgressBar到底扮演了一个什么样的角色。
- ProgressBar其实扮演的是通知的角色,是一个通知控件。我们是不是可以用一个抽象的方式来表达一个通知,而不需要一个具体的控件来表达通知。
ProgressBar其实是一个通知控件,扮演了通知的角色。
我们可以把ProgressBar抽象成通知,然后让FileSplitter依赖通知抽象类,就能解决上一个问题。
class IProgress{//用IProgress表达一种抽象的通知//而ProgressBar是一种具体通知控件public:virtual void DoProgress(float value) = 0;virtual ~IProgress(){}}//filesplitter.cppclass FileSplitter {string m_filePath;int m_fileNumber;//ProgressBar* m_progressBar;//ProgressBar的角色其实是“通知”//但我们不必用一个具体的控件(ProgressBar)来表示通知//可以用IProgress表达一种抽象的通知IProgress* m_iprogress; //抽象的通知public:FileSplitter(const string& filePath, int fileNumber, IProgress* iprogress) :m_filePath(filePath),m_fileNumber(fileNumber),m_iprogress(iprogress){}void split(){//1.读取大文件//2.分批次向小文件中写入for (int i = 0; i < m_fileNumber; i++){//...//进度条展示if (m_iprogress != nullptr) {float progressValue = m_fileNumber;progressValue = (i + 1) / progressValue;m_iprogress->DoProgress(progressValue);}}}};//mainform.cppclass MainForm : public Form, public IProgress {//C++虽然支持多继承,但不推荐使用多继承,它会带来很多很多比较复杂的耦合问题//但C++推荐一种多继承的形式:一个是主的继承类,其他都是接口(or抽象基类)//这也是后来的语言所提倡的(它们也是借鉴C++的经验):单继承是父类,其他是实现接口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, this);splitter.split();}//实现IProgress接口virtual void DoProgress(float value) {progressBar->setValue(value);}};
如此,我们已经相当成功的将此案例满足了依赖倒置原则。把原来一个紧耦合变成了一个松耦合。
FileSplitter已经不再依赖一个具体的界面类。所以,我们可以独立的编译FileSpliiter- 界面类MainForm不存在之前,就可以编译FileSplitter。如此,将来我们就可以把FileSplitter放在具有窗口的界面,也可以放在命令行等等。
进一步优化:把FileSplitter中设置进度的代码抽成一个函数,甚至把它抽成虚函数,以供其子类重载
class IProgress{public:virtual void DoProgress(float value) = 0;virtual ~IProgress(){}}//filesplitter.cppclass FileSplitter {string m_filePath;int m_fileNumber;IProgress* m_iprogress; //抽象的通知public:FileSplitter(const string& filePath, int fileNumber, IProgress* iprogress) :m_filePath(filePath),m_fileNumber(fileNumber),m_iprogress(iprogress){}void split(){//1.读取大文件//2.分批次向小文件中写入for (int i = 0; i < m_fileNumber; i++){//...//更新进度条float progressValue = m_fileNumber;progressValue = (i + 1) / progressValue;onProgress(progressValue);//1. 这样一改之后,发现代码更清楚了。}}protected://2. 甚至有时候会写成虚函数,以便子类后续去改写virtual void onProgress(float value) {if (m_iprogress != nullptr) {m_iprogress->DoProgress(progressValue);}}};//mainform.cppclass 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());FileSplitter splitter(filePath, number, this);splitter.split();}//实现IProgress接口virtual void DoProgress(float value) {progressBar->setValue(value);}};
支持多个观察者:观察者模式
还有一个问题,如果需要支持多个通知呢?
- 也就是需要多个观察者
- 我们现在只支持一个观察者MainForm(或者讲ProgressBar)
比如说这里多了一个类ConsoleNotifier,有一个问题:FileSpliiter只能接收一个观察者(FileSplitter构造函数中的第三个参数)
//多填了一个观察者:控制台观察者class ConsoleNotifier : public IProgress{public:virtual void DoProgress(float value) {cout << value << endl;}}//mainform.cppclass 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());//【问题】但是FileSpliiter只能接收一个观察者(第三个参数)FileSplitter splitter(filePath, number, this);splitter.split();}//实现IProgress接口virtual void DoProgress(float value) {progressBar->setValue(value);}};
优化:支持多个观察者
class IProgress{public:virtual void DoProgress(float value) = 0;virtual ~IProgress(){}}//filesplitter.cppclass FileSplitter {string m_filePath;int m_fileNumber;List<IProgress*> m_iprogressList; //抽象的通知:支持多个观察者public://要支持多个观察者,这里就不建议构造时传入了,建议使用add一次一次添加进来FileSplitter(const string& filePath, int fileNumber /*, IProgress* iprogress*/) :m_filePath(filePath),m_fileNumber(fileNumber)/*,m_iprogress(iprogress)*/ {}//add、remove表示我们支持多个观察者void addIProgress(IProgress* iprogress) {m_iprogressList.push_back(iprogress);}void removeIProgress(IProgress* iprogress) {m_iprogressList.remove(iprogress);}void split(){for (int i = 0; i < m_fileNumber; i++){//...float progressValue = m_fileNumber;progressValue = (i + 1) / progressValue;onProgress(progressValue);}}protected:virtual void onProgress(float value) {//通知每个观察者for(auto it=m_iprogressList.begin() it!=m_iprogressList.end(); ++it) {(*it)->DoProgress(progressValue);}}};//多填了一个观察者:控制台观察者class ConsoleNotifier : public IProgress{public:virtual void DoProgress(float value) {cout << value << endl;}}//mainform.cppclass 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);spliiter.addIProgress(&cn);//注意:在这个过程中,内存管理要弄好splitter.split();//当然也可以移除观察者splitter.removeIProgress(this);}//实现IProgress接口virtual void DoProgress(float value) {progressBar->setValue(value);}};
到目前为止,就算真正的观察者模式。
模式定义
定义对象间的一种一对多(变换)的依赖关系,以便当一个对象(Subject)的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。——《设计模式》GOF
结构
红色部分是稳定的部分,是系统依赖的部分。蓝色部分是为了支持一对多的变化,也就是具体观察者的实现,具体主体对象的实现。
Observer相当于IProgress,Observer::Update()相当于IProgress::DoProgressConcreteSubject相当于FileSplitter,简单来说就是主体对象,被监听的对象ConcreteObserver相对于MainForm或者ConsoleNotifiy。即具体的观察者。Subject是什么呢?- 官方建议把
Splitter抽象成Subject,并将FileSplitter的三个函数抽成基类的函数。即Subject::Attach()相当于FileSplitter::add;Subject::Detach()相当于FileSpliiter::remove;Subject::Notify()相当于FileSplitter:onProgress - 当然,也有一些直接把Subject、ConcreteSubject合并成一个类,把Subject的三个函数直接写在ConcreteSubject内里面,比如我们所举的例子。
Subject、ConcreteSubject合起来相当于FileSplitter要点总结
要点一:使用面向对象的抽象,Observer模式使得我们可以独立地改变目标与观察者,从而使二者之间的依赖关系达致松耦合。
- 官方建议把
独立:表达的就是两者之间松耦合的关系,谁也不依赖谁,我改变不会影响到你,你改变也不会影响到我
- 对于目标(FileSplitter),添加多少个观察者,随你便(直接addIProgress就好)
- 对于观察者,适应多种多样观察者。是进度条,还是数字;是窗口,还是命令行程序都行。观察者怎么变,目标都不用变
要点二:目标发送通知时,无需指定观察者,通知(可以携带通知信息作为参数)会自动传播
- 发送通知时(
FileSplitter::onProgress),FileSplitter不知道谁是观察者,通过抽象通知方法,将消息自动传播到具体的观察者
要点三:观察者自己决定是否需要订阅通知,目标对象对此一无所知
- 在
MainForm::Button1_click()的splitter.addIProgress中,观察者MainForm自己决定是否要订阅通知 - 目标对象(
FileSplitter)对此一无所知
要点四:Observer模式是基于事件的UI框架中非常常用的设计模式,也是MVC模式的一个重要组成部分
举例:
- C#中的Event事件模式就是观察者模式的一种表现
注意:观察者模式需要大家常用常思考,它可以在代码上有不同的展现形式。但是最关键的是:那个抽象的通知依赖关系
其他说法
【观察者】Observer
理念:将一个操作或一个系统分成两部分(观察者、实际运行的部分),把用户界面和业务逻辑进行分开处理。如显示器主机箱的关系,显示器只显示,主机负责所有处理与运算。

逻辑层面:
- Observer将一些控制信息反馈给用户
- Subject实现业务逻辑
物理层面:
- Observer一个或多个观察者类
- Subject主体类
- Container容器
- Register(this):一个观察者要得到主体类的数据的话,首先观察者需要提出申请(申请得到你的数据,申请作为一个前台,显示你的数据);之后,Subject会将观察者添加到一个Container中;主体数据有变化就立马告诉Container,容器内的所有成员就能得到主体变化的信息
- Unregister(this):注销观察
【例子】在一个公文处理系统中,开发者定义了一个公文类OfficeDoc,当公文的内容或状态发生变化时,关注此OfficeDoc类的相应DocExplorer对象,都更新其自身的状态。一个OfficeDoc对象能够关联一组DocExplorer对象,
