模板方法属于“组件协作”模式,其实就是通过晚绑定的方式来实现框架与应用程序之间的松耦合。属于组件协作模式的还有:策略模式(Strategy)、事件模式(Observer/Event)
模板方法(Template Method)
模板方法应用的背景
在软件构建的过程中,对于某一项任务,它常常有稳定的整体操作结构,但各个子步骤却又很多变化的需求,或者由于固有的原因(比如框架与应用之间的关系)而无法和任务的整体结构同时实现。
比如有一项任务需要执行5步,其中1、3、5步是稳定的,是不会变更的,而2、4步是变化的。如果采用模板方法去实现,那么应该将主流程的运行过程放在一个类里,由固定的代码实现,1、2、3、4、5步的函数定义,也放在这个类里,但其中2和4设置成虚函数,由继承该类的子类实现,如此,使用此类来执行任务,只需实现虚函数2和4的具体内容。
错误样例:
原先Library开发人员(框架构建者)需要实现:
– 开发1、3、5三个步骤
Application开发人员需要实现:
– 开发2、4两个步骤
– 自行调用12345,实现程序主流程
正确样例:
原先Library开发人员(框架构建者)需要实现:
– 开发1、3、5三个步骤
– 实现程序主流程
Application开发人员需要实现:
– 开发2、4两个步骤
定义:确定一个操作中算法的骨架(稳定的),而将一些步骤(变化的)延迟到子类中。模板方法使得子类可以不改变(即复用)一个算法的结构即可重定义(override)该算法的某些特定步骤。
策略模式(Strategy)
在软件构建过程中,某些对象使用的算法可能多种多样,经常改变,如果将这些算法都编码到对象中,将会使对象变得异常复杂,而且有时候支持不适用的算法也是一个性能负担。(换句话说就是,if-else罗列一大堆,而且场景将来还会扩展,if else还会增多)
比如,现在需要根据不同的国家的税法去计算一个税的金额,如果通过if-else的方法,判断不同国家执行对应代码,可能要依次判断好多行,并随着业务扩展,新国家税法加入,导致if-else会不断增加。
那么这个时候就需要考虑策略模式,构造一个税法的基类,定义虚函数计算税法,构造多个不同国家的税法子类继承基类。再构造一个新类,控制流程执行,新类里有税法基类的指针,采用多态的方式执行不同国家的税法。如此,便可在有新的国家业务出现的时候,只需新增税法子类,其他代码继续复用。
模式定义:定义一系列算法,将他们一个个封装起来,并且使他们(变化的)可相互替换。该模式使得算法可独立于使用他们的客户程序(稳定的)而变化。
观察者模式(Observer/Event)
在软件构建过程中,我们需要为某些对象建立一种“通知依赖关系”。一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知。如果这样的依赖关系过于紧密,将使软件不能很好地抵御变化。
使用面向对象技术,可以将这种依赖关系弱化,并形成一种稳定的依赖关系,从而实现软件体系结构的松耦合。
错误示范:
// 需求:点一下按键,执行文件的切割,分成多个小份,但需要更新打包进度条
class FileSplitter{
string m_filePath;
int m_fileNumber;
ProgressBar *m_progressBar; // m_progressBar是实现细节,是具体的通知控件,FileSplitter对它产生了编译时依赖
public:
FileSplitter(const string &filePath, int fileNumber, ProgressBar *progressBar):
m_filePath(filePath),
m_fileNumber(fileNumber),
m_progressBar(progressBar) {
}
void split(){
// ...
for(int i = 0; i < m_fileNumber; ++i){
// ...分成小份写入
float curr = i+1;
m_progressBar->setValue( curr / m_fileNumber ); // 更新
}
}
}
class MainForm : public Form{
TextBox *txtFilePath;
TextBox *txtFileNumber;
public:
void Button01_Click(){
string filePath = txtFilePath->getText(); // 假设点击即可获取的参数
int number = atoi(txtFileNumber->getText().c_str());
FileSplitter splitter(filePath, number);
splitter.split(); // 执行文件的切割打包
}
}
上面这个示范,违背了第一大原则:依赖倒置原则,即高层模块不能依赖低层模块,二者都应该依赖抽象,抽象不能依赖实现细节。
class IProgress{
public:
virtual void DoProgress(float values)=0;
virtual ~IProgress(){}
}
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(){
// ...
for(int i = 0; i < m_fileNumber; ++i){
// ...分成小份写入
float curr = i+1;
m_iprogress->DoProgress( curr / m_fileNumber ); // 更新
}
}
}
class MainForm : public Form, public IProgress{ // 多继承时,一般最好只能一个主的继承类,其他都是接口或抽象基类
TextBox *txtFilePath;
TextBox *txtFileNumber;
ProgressBar *progressBar;
public:
void Button01_Click(){
string filePath = txtFilePath->getText(); // 假设点击即可获取的参数
int number = atoi(txtFileNumber->getText().c_str());
FileSplitter splitter(filePath, number, this);
splitter.split(); // 执行文件的切割打包
}
virtual void DoProgress(float values){
progressBar->setValue(value);
}
}
至此,FileSplitter就没有耦合一个界面类MainForm。当然想要实现接受多个通知,可以将FileSplitter里的m_iprogress用容器装起来,封装add和remove函数。
模式定义:定义对象间的一种一对多(变化的)的依赖关系,以便当一个对象(Subject)的状态发生变化时,所有依赖于它的对象都得到通知并自动更新。