属于类别
- 组件协作模式
动机
在软件构建过程中,我们需要为某些对象建立一种“通知依赖关系”——一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都得到通知。如果这样的依赖关系过于紧密,将使软件不能很好地抵御变化。
使用面向对象技术,可以将这种依赖关系弱化,并形成一种稳定的依赖关系。从而实现软件体系结构的松耦合。
代码
文件分割器
需求:实现一个文件分割器
//MainForm.cpp
class 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.cpp
class 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.cpp
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.cpp
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++){
//...
//进度条展示
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.cpp
class 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.cpp
class 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.cpp
class 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.cpp
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());
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.cpp
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());
//【问题】但是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.cpp
class 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.cpp
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);
spliiter.addIProgress(&cn);
//注意:在这个过程中,内存管理要弄好
splitter.split();
//当然也可以移除观察者
splitter.removeIProgress(this);
}
//实现IProgress接口
virtual void DoProgress(float value) {
progressBar->setValue(value);
}
};
到目前为止,就算真正的观察者模式。
模式定义
定义对象间的一种一对多(变换)的依赖关系,以便当一个对象(Subject)的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。——《设计模式》GOF
结构
红色部分是稳定的部分,是系统依赖的部分。蓝色部分是为了支持一对多的变化,也就是具体观察者的实现,具体主体对象的实现。
Observer
相当于IProgress
,Observer::Update()
相当于IProgress::DoProgress
ConcreteSubject
相当于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对象,