一、模式動機
- 在軟件構建中,我們需要爲某些對象構建一種"通知依賴"關係,一個對象(目標對象)狀態改變時,所有依賴對象(觀察者)都將得到通知;
- 使用OOP(面向對象編程)技術,可以將這種依賴關係弱化,並形成一種穩定的依賴關係,從而實現軟件體系接口的松耦合;
二、程序示例
2.1 使用設計模式前
這裏,我們設計一個文件分割的例子,雖然現在用的不多,因爲現在的存儲介質容量都相對很大,但是作爲一個例子來說明設計模式的解耦思維.
#include <stdio.h>
#include <string>
#include <list>
using namespace std;
// 聲明一個文二建分割器Class
class FileSplitter
{
public:
// 使用初始化列表初始化私有成員變量
FileSplitter(const std::string& filePath,int fileNumber):m_filePath(filePath),m_fileNumber(fileNumber){}
public:
void split()
{
// 1. 讀取大文件
// 2. 分批次從向小文件中寫入
for (int i = 0; i < m_fileNumber; i++)
{
// ... 文件寫入工作
}
}
private:
std::string m_filePath;
int m_fileNumber;
};
// 聲明一個窗體類,繼承自窗體基類,類似Qt或MFC,其中Form類是僞代碼假設
class Form{};
class TextBox{};
class MainForm:public Form
{
public:
void Button_Click() // 一個點擊事件方法
{
std::string filePath = txtFilePath->getText();
int num = atoi(txtFileNumber->getText.c_str()); // 文本轉化爲整型
FileSplitter splitter(filePath,num);
splitter.split();
}
private:
TextBox* txtFilePath;
TextBox* txtFileNumber;
};
int main(){
MainForm main_window; // 然後做點擊操作
rentrun 0;
}
到此,一個簡單的文件分割窗口程序僞代碼就寫好了。如果現在用戶提出一個需求:如果文件過大,文件分割可能持續較長時間,能否設置一個進度條,實時查看文件分割的進度?可能實現的解決方案:
- 在MainForm裏添加一個進度條,如ProgressBar* m_progressBar;
- 然後在在split()方法for循環內添加:
if(m_progessBar!=nullptr)
m_progessBar->setValue((i+1)/(double)m_fileNumber*100);
2.2 問題思考
如果將來某天,用戶提出需求,不在使用進度條了,要使用文本框或者控制檯的方式怎麼辦?按照當前的設計方式,我們一定在代碼迭代時需要同時修改MainForm和FileSplitter中的代碼,這就不是運行時依賴了,而是典型的編譯時依賴,這就不是一種穩定的代碼設計方式.
從代碼設計原則上看,上述代碼違背了OOP八大設計原則的第一條:依賴倒置原則(DIP),即:
- 高層模塊(穩定)不應該依賴於底層(變化),二者都應該依賴於抽象;
- 抽象不應該依賴細節實現變化,實現細節應該依賴於抽象;
總的來說,上述實現方式是編譯時依賴而不是運行時依賴的,最完美的程序設計需要運行時依賴. 我們不能保證以後的進度通知時通過哪種具體的方式,甚至通知的數量,因此我們要在高層和底層之間增加一層抽象.
2.3 使用設計模式後
思路:其中ProgressBar體現了一種消息的通知,而不是具體的展現方式.
#include <stdio.h>
#include <string>
#include <list>
using namespace std;
// 使用OOP多態,在高層和底層之間增加一層抽象
class IProgress {
public:
virtual void DoProgress(float value) = 0; // 從一個具體的控件,到一個消息通知的抽象
virtual ~IProgress(){}
};
class FileSplitter:public IProgress // 底層細節繼承中間抽象層
{
public:
virtual ~FileSplitter() {}
// 使用初始化列表初始化私有成員變量
FileSplitter(const std::string& filePath,int fileNumber):m_filePath(filePath),m_fileNumber(fileNumber){}
public:
void split()
{
// 1. 讀取大文件
// 2. 分批次從向小文件中寫入
for (int i = 0; i < m_fileNumber; i++)
{
// ... 文件寫入工作
if(m_progessBar!=nullptr)
this->DoProgress((i+1)/(double)m_fileNumber*100);
}
}
void DoProgress(float value){ /* 消息通知的代碼 */} // 實現進度消息的通知
// 使用容器添加多個觀察者對象
void addIProgress(IProgress* iprogress)
{
m_iprogressList.push_back(iprogress);
}
void removeIProgress(IProgress* iprogress)
{
m_iprogressList.remove(iprogress);
}
protected: // 使用容器管理多個容器
virtual void onProgress(float value)
{
for (auto iter = m_iprogressList.begin; iter != m_iprogressList.end; iter++)
iter->DoProgress(value);
}
private:
std::string m_filePath;
int m_fileNumber;
IProgress* m_iprogress; // 使用抽象
std::list<IProgress*> m_iprogressList; // 使用容器改良,可以同時通知多個觀察者
};
// 界面窗體
class MainForm:public IProgress // 使用抽象後
{
public:
void Button_Click() // 一個點擊事件方法
{
std::string filePath = txtFilePath->getText();
int num = atoi(txtFileNumber->getText.c_str()); // 文本轉化爲整型
FileSplitter splitter(filePath,num);
splitter.split();
}
public:
// Override 通過繼承的方式實現虛方法
virtual void DoProgress(float value){
// 實現具體消息通知的代碼
}
private:
TextBox* txtFilePath;
TextBox* txtFileNumber;
};
### 2.4 結果分析
看起來觀察者模式與策略模式有點相似,但在死路上有本質的區別。策略模式強調擴展性,而觀察者模式強調依賴倒置.
## 三、要點總結
1. 使用OOP的抽象,觀察者模式使得我們可以獨立地改變目標和觀察者,從而使兩者之間的依賴關係達到松耦合;
2. 目標發送通知時,無需指定觀察者,通知(可以攜帶通知信息作爲參數)會自動傳播;
3. 觀察者自己決定是否需要訂閱通知,目標對象對此一無所知;
4. 觀察者模式基於事件的UI框架中非常常用的設計模式,一個MVC模式的一個重要組成部分;
5. 非UI框架時,在回調函數中也使用非常多.