**"组件协作"模式:**现代软件专业分工之后的第一个结果是“框架与应用程序的划分”,“组件协作”模式是通过晚期绑定,来实现框架和应用程序之间的松耦合,是二者之间协作常用的模式。

Template Method

动机

  • 在软件构建过程中,对于某一项任务,它常常有稳定的整体操作结构,但各个子步骤去有很多改变的需求,或者由于固有原因(比如框架和应用之间的关系)而无法和任务的整体结构同时实现。
  • 如果在确定稳定操作结构的前提下,来灵活应对各个子步骤的变化或晚期实现需求

定义

Define the skeleton of an algorithm in an operation(稳定), deferring(变化) some steps to subclasses. Template Method lets subclass redefine(重写 override) certain steps of an algorithm without changing(复用) the algorithm’s structure.

实现

结构化软件设计流程

template1_lib.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 程序库开发人员
class Library {
public:
void Step1() {
//...
}

void Step3() {
//...
}

void Step5() {
//...
}
};

template_app.cpp

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
// 应用程序开发人员
class Application {
public:
bool Step2() {
//...
}

void Step4() {
//...
}
};

int main() {
Library lib();
Application app();

lib.Step1();

if (app.Step2()) {
lib.Step3();
}

for (int i = 0; i < 4; i++) {
app.Step4();
}

lib.Step5();
}

应用程序开发人员需要控制程序主流程。
如果程序主流程是稳定,那么放到程序库中是更合适的。

但是程序库开发早,应用程序开发晚,怎么实现“晚绑定”呢?—— 虚函数 / 函数指针。

image-20240328102218519

Template Method 面向对象软件设计流程

template2_lib.cpp

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
// 程序库开发人员
class Library {
public:
// 稳定 template method
void Run() {
Step1();

if (Step2()) { // 支持变化 ==> 虚函数的多态调用
Step3();
}

for (int i = 0; i < 4; i++) {
Step4(); // ֧支持变化 ==> 虚函数的多态调用
}

Step5();
}
virtual ~Library() {} // 任何基类都要写虚析构函数

protected:
void Step1() { // 稳定
//.....
}
void Step3() { // 稳定
//.....
}
void Step5() { // 稳定
//.....
}

virtual bool Step2() = 0; // 变化
virtual void Step4() = 0; // 变化
};

template2_app.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 应用程序开发人员
class Application : public Library {
protected:
virtual bool Step2() {
//... 子类重写实现
}

virtual void Step4() {
//... 子类重写实现
}
};

int main() {
Library* pLib = new Application(); // 多态指针
lib->Run();

delete pLib;
}

通过 Template Method,变成了程序库开发人员控制程序主流程。

C 语言严格上也有晚绑定机制,只是用的是函数指针(抽象性比如虚函数,但是虚函数本质上也是虚函数表上挂了函数指针)。

image-20240328102314515

扩展:如果没有稳定的、可复用的算法骨架,那么假设就不成立,不能使用 Template Method。

结构

  • 稳定骨架对应 Run()

总结

  1. Template Method 模式是一个非常基础性的设计模式,在面向对象系统中有大量的应用。它用最简洁的机制(虚函数的多态性)为很多应用程序框架提供了灵活的扩展点,是代码复用方面的基本实现结构。

  2. 除了可以灵活应对子步骤的变化外,“不要调用我,让我来调用你”的反向控制是 Template Method 的典型应用。

  3. 在具体实现方面,被 Template Method 调用的虚方法可以具有实现,也可以没有任何实现(抽象方法、纯虚方法),但一般推荐将它们设置为 protected 方法。

    为啥要设为 protected

    这些方法往往是要放到一个流程中才有意义,单独作为一个 public 接口让用户去使用意义不大,因此设置为 protected


Strategy

Strategy 也称为 Policy

动机

  • 在软件构建过程中,某些对象使用的算法可能多种多样,经常改变,如果将这些算法都编码到对象中,将会使对象变得异常复杂;而且有时候支持不使用的算法也是一个性能负担。
  • 如何在运行时根据需要透明地更改对象的算法?将算法与对象本体解耦,从而避免上述问题?

定义

Define a family of algorithm, encapsulate each one, and make them interchangeable(变化). Strategy lets the algorithm vary independently from clients(稳定) that use it(扩展,子类化).

实现

Naive

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
enum TaxBase {
CN_Tax,
US_Tax,
DE_Tax,
FR_Tax // 更改
};

class SalesOrder {
TaxBase tax;

public:
double CalculateTax() {
//...

if (tax == CN_Tax) {
// CN***********
} else if (tax == US_Tax) {
// US***********
} else if (tax == DE_Tax) {
// DE***********
} else if (tax == FR_Tax) { // 更改
//...
}

//....
}
};

这段代码初看没啥问题,但作为程序员,不能静态的看问题,要有时间轴观念——考虑未来的变化
新的需求将导致:重新修改、重新编译、重新测试、重新部署。

违反“开放封闭原则”:应该对扩展开放,对修改封闭。

Strategy

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
class TaxStrategy {
public:
virtual double Calculate(const Context& context) = 0;
virtual ~TaxStrategy() {} // 一定要写多态类的虚析构函数
};

class CNTax : public TaxStrategy {
public:
virtual double Calculate(const Context& context) {
//***********
}
};

class USTax : public TaxStrategy {
public:
virtual double Calculate(const Context& context) {
//***********
}
};

class DETax : public TaxStrategy {
public:
virtual double Calculate(const Context& context) {
//***********
}
};

// 扩展
//*********************************
class FRTax : public TaxStrategy {
public:
virtual double Calculate(const Context& context) {
//.........
}
};

class SalesOrder {
private:
TaxStrategy* strategy; // 一般要实现多态变量, 就要用指针

public:
SalesOrder(StrategyFactory* strategyFactory) {
this->strategy = strategyFactory->NewStrategy();
}
~SalesOrder() { delete this->strategy; }

public
double CalculateTax() {
//...
Context context();

double val = strategy->Calculate(context); // 多态调用
//...
}
};

  • 把之前的一个个算法,变成了这里的一个个 TaxStrategy 的子类。

  • 工程实现应该把一个类放到一个文件中,这里为了方便表示,就不那样做了。

  • 设计模式中“复用”指的是编译单位,二进制层面的复用性,是编译测试后原封不动;源码级别的复制粘贴不叫复用。

结构

  • Context 对应 SalesOrder
  • Strategy 对应 TaxStrategy

总结

  1. Strategy 及其子类为组件提供了一系列可重用的算法,从而可以使得类型在运行时方便地根据需要在各个算法之间进行切换。

  2. Strategy 模式提供了用条件判断语句以外的另一种选择,消除条件判断语句,就是在解耦合。含有许多条件判断语句的代码通常都需要 Strategy 模式。

    除非条件绝对稳定不变,才不需要考虑 Strategy,比如一周七天的判断。

  3. 如果 Strategy 对象没有实例变量,那么各个上下文可以共享同一个 Strategy 对象,从而节省对象开销。(不是特别重要)


Observer / Event

Observer 也被称为 Event, Dependents, Publish-Subscribe

动机

  • 在软件构建过程中,我们需要为某些对象建立一种“通知依赖关系” ——一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知。如果这样的依赖关系过于紧密,将使软件不能很好地抵御变化。
  • 使用面向对象技术,可以将这种依赖关系弱化,并形成一种稳定的依赖关系。从而实现软件体系结构的松耦合。

定义

Define a one-to-many(变化) dependency between objects so that when one object changes, all its dependents are notified and update automatically.

实现

Naive

FileSplitter1.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class FileSplitter {
string m_filePath;
int m_fileNumber;
ProgressBar* m_progressBar; // 违反依赖倒置原则,不应该依赖具体的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++) {
//...
float progressValue = m_fileNumber;
progressValue = (i + 1) / progressValue;
m_progressBar->setValue(progressValue);
}
}
};

MainForm1.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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();
}
};

思考问题在哪?(违法哪条原则)

这里 MainForm 依赖具体的 progressBar 是违反依赖倒置原则的:实现细节应该依赖抽象(稳定)。

Observer

这里的代码需要结合下一节的结构图来看。

FileSplitter2.cpp

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
// Obeserver 抽象基类
class IProgress {
public:
virtual void DoProgress(float value) = 0;
virtual ~IProgress() {}
};

// Subject (这里没有区分抽象基类和具体类)
class FileSplitter {
string m_filePath;
int m_fileNumber;

// ProgressBar *m_progressBar; // 具体通知控件
List<IProgress*> m_iprogressList; // 抽象通知机制, 支持多个观察者

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++) {
//...

float progressValue = m_fileNumber;
progressValue = (i + 1) / progressValue;
onProgress(progressValue); // 更新进度条
}
}

void addIProgress(IProgress* iprogress) {
m_iprogressList.push_back(iprogress);
}

void removeIProgress(IProgress* iprogress) {
m_iprogressList.remove(iprogress);
}

protected:
virtual void onProgress(float value) {
List<IProgress*>::iterator itor = m_iprogressList.begin();

while (itor != m_iprogressList.end()) {
(*itor)->DoProgress(value); // 更新进度条
itor++;
}
}
};

MainForm2.cpp

形式上,把 setValue 通过 DoProgress 从 FilerSplitter搬到了 MainForm2。但实际上变化更大,这将不良耦合变成了良好的耦合。

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
// C++唯一推荐的多继承实现:单继承一个父类,然后其他的是接口
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); // 订阅通知
splitter.addIProgress(&cn); // 订阅通知

splitter.split();

splitter.removeIProgress(this);
}

virtual void DoProgress(float value) { progressBar->setValue(value); }
};

class ConsoleNotifier : public IProgress {
public:
virtual void DoProgress(float value) { cout << "."; }
};

结构

image-20240328103703178

  • Observer 相当于 IProcess,里面的 Update() 相当于 doProgress()
  • GOF 中推荐写一个基类,包含 Attach()Detach()Notify(),上述代码中把 Subject 和 ConcreteSubject合二为一了。这三个方法分别对应 addIProgress()removeIProgress()onProgress()
  • ConcreteObserver 相当于 MainForm 和 ConsoleNotifier。

总结

  1. 使用面向对象的抽象,Observer模式使得我们可以独立地改变目标和观察者,从而使得二者之间的依赖关系达致松耦合。

    可以添加多个具体的观察者,上面侯松的部分是不用变的。

  2. 目标发送通知时,无需指定观察者,通知(考研携带通知信息作为参数)会自动传播。

  3. 观察者自己决定是否需要订阅通知,目标对象对此一无所知。

Observer 模式是基于事件的UI框架中非常常用的设计模式,也是MVC模式中的一个重要组成部分。