设计模式-观察者模式(Observer)

Observer 模式要解决的问题为:建立一个一(Subject)对多(Observer)的依赖关系,并且做到当”一“变化的时候,依赖这个“一”的多也能够同步改变。

问题提出

假设要实现一个文件分割器,将一个大文件分割成若干个小文件。下面将通过对此功能一步步迭代的实现来揭示观察者模式的细节。

1.0 版本

这是一种最简单的实现方法(注意:这只是伪代码,旨在说明设计模式的思想)

  • FileSplitter 是文件分割器的类
  • MainForm 是主窗口的类
  • 在按钮点击事件中调用文件分割器类进行分割。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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)
{
//......
}
}
};

class MainForm : public Form
{
TextBox* txtFilePath;
TextBox* txtFileNumber;
public:
void Button1Click {
string filePath = txtFilePath->getText();
int number = atoi(txtFileNumber->getText().c_str());

FileSplitter splitter(filePath, number);

splitter.split();
}
};

2.0 版本

现在有一个新的需求,如果文件太大,文件分割势必需要更长的时间,为了更直观地显示分割的过程,需要为分割的过程添加一个进度条提示。

使用一个进度条控件的指针来将控件变量的地址传给文件分割器的对象,从而实现进度的通知。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class FileSplitter
{
string m_filePath;
int m_fileNumber;
ProgressBar* m_pProgressBar; // 具体的通知控件
public:
FileSplitter(const string& filePath, int fileNumber, ProgressBar* progressBar) :
m_filePath(filePath),
m_fileNumber(fileNumber),
m_pProgressBar(progressBar) // 初始化
{

}

void split()
{
// 1. 读取大文件
// 2. 分批次向小文件写入
for (int i = 0; i < m_fileNumber; ++i)
{
//......
if (nullptr != m_pProgressBar)
{
m_pProgressBar->setValue((i+1) / m_fileNumber); // 更新进度条
}
}
}
};

class MainForm : public Form
{
TextBox* txtFilePath;
TextBox* txtFileNumber;
ProgressBar m_progressBar;
public:
void Button1Click {
string filePath = txtFilePath->getText();
int number = atoi(txtFileNumber->getText().c_str());

FileSplitter splitter(filePath, number, &m_progressBar);

splitter.split();
}
};

3.0 版本

上面的代码看上去很美好,实际上依然存在一个问题:如果现在这个文件分割类不再用于当前的界面框架,或者干脆就不再是含有界面的程序,那么 FileSplitter 类中的进度条类型的指针便不再有效,代码移植时要修改这个指针的类型,还要修改构造函数、split 方法中的更新进度操作都要作出修改(上面代码中有注释的三处)。下面的代码采用了观察者模式的设计方法,可以便于扩展。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class IProgress
{
public:
virtual void DoProgress(double val) = 0;
virtual ~IProgress() {

}
};

class FileSplitter
{
string m_filePath;
int m_fileNumber;
// ProgressBar* m_pProgressBar; // 具体的通知控件
IProgress* m_iProgress; // 抽象的通知
public:
FileSplitter(const string& filePath, int fileNumber, ProgressBar* progressBar) :
m_filePath(filePath),
m_fileNumber(fileNumber),
m_iProgress(iProgress) // 初始化
{

}

void split()
{
// 1. 读取大文件
// 2. 分批次向小文件写入
for (int i = 0; i < m_fileNumber; ++i)
{
//......
if (nullptr != m_iProgress)
{
m_iProgress->DoProgress( (i+1) / m_fileNumber ); // 更新进度条
}
}
}
};

class MainForm : public Form public IProgress
{
TextBox* txtFilePath;
TextBox* txtFileNumber;
ProgressBar m_progressBar;
public:
void Button1Click {
string filePath = txtFilePath->getText();
int number = atoi(txtFileNumber->getText().c_str());

FileSplitter splitter(filePath, number, this);

splitter.split();
}

void DoProgress(double val)
{
m_progressBar.SetValue(val);
}
};

此段代码用抽象的通知代替了具体的通知控件,MainForm 类通过继承 IProgress 类来实现一个 DoProgress 的接口,把这样一个类的指针直接传给 FileSplitter 类的对象,FileSplitter 类中的 split 方法可以用统一的接口调用 DoProgress 方法来实现进度的通知。

4.0 版本

下面是一个实现多个观察者的例子,在进度改变时同时通知 MainForm 和 Console 使其发生改变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
class IProgress
{
public:
virtual void DoProgress(double val) = 0;
virtual ~IProgress() {

}
};

class FileSplitter
{
string m_filePath;
int m_fileNumber;
// ProgressBar* m_pProgressBar; // 具体的通知控件
list<IProgress*> m_iProgressList; // 抽象的通知列表
public:
FileSplitter(const string& filePath, int fileNumber) :
m_filePath(filePath),
m_fileNumber(fileNumber),
{

}

void add_iPorgress(IProgress* iProgress) {
m_iProgressList.push_back(iProgress);
}
void remove_iProgress(IProgress* iProgress) {
m_iProgressList.remove(iProgress);
}

void split()
{
// 1. 读取大文件
// 2. 分批次向小文件写入
for (int i = 0; i < m_fileNumber; ++i)
{
//......
for (auto it : m_iProgressList)
it->DoProgress( (i+1) / m_fileNumber ); // 更新进度条
}
}
};

class MainForm : public Form public IProgress
{
TextBox* txtFilePath;
TextBox* txtFileNumber;
ProgressBar m_progressBar;
public:
void Button1Click {
string filePath = txtFilePath->getText();
int number = atoi(txtFileNumber->getText().c_str());

FileSplitter splitter(filePath, number);
splitter.add_iPorgress(this);
splitter.add_iPorgress(&con);

splitter.split();
}

void DoProgress(double val)
{
m_progressBar.SetValue(val);
}
};

class Console : public IProgress
{
public:
void DoProgress(double val) {
cout << "...";
}
};

总结

下面是观察者模式(Observer Pattern)的类图

Observer