我使用过一个简单的后台服务框架.这个框架上手很容易,我只需要继承一个基类,同时实现,或重写(override)基类声明的几个接口(这些接口声明为虚函数,或者纯虚函数),然后调用基类定义好的run()函数,便可以将框架代码运行起来.run函数做的事情,是依序调用上述的几个接口:
class Service {
public :
int run(){
// ....
step1(); // 收包 , 解包
step2(); // 业务逻辑处理
step3(); // 回包
step4(); //资源释放
//....
}
protected:
virtual int step1(){
// 收包,解包逻辑实现
//...
}
virtual int step3(){
// 回包逻辑实现
//...
}
virtual int step4(){
//资源释放
//...
}
virtual int step2() =0 ; //纯虚函数,派生类实现
}
其中收包,解包,回包,释放资源等动作,框架会提供一份实现,由于我们有时候会采用其他的数据协议,所以基类也将收包回包等函数声明为虚函数,允许我们针对新的协议进行函数的重写(override).而对于业务逻辑处理函数,也就是step2,框架无法为我们实现,我们需要根据具体的业务需求来实现该函数,在派生类中来实现step2函数:
class MyService : public Service{
int step2(){
// 具体业务逻辑的实现
}
}
派生类实现了step2函数后,通过调用run函数来运行程序:
void main (){
//...准备工作
Service * myService = new MyService();
if( myService->run()){
//...后续处理
}
// ...
}
我们的后台服务框架例子中,run函数定义了一个服务的稳定执行步骤,但某个步骤有着特定的需求无法马上定义,需要延迟到派生类中去实现,这时候就需要采用模板方法模式.模板方法模式要解决的问题是:如何在确定稳定操作结构的前提下,灵活地应对各个子步骤的变化或者晚期实现需求?
李建忠老师曾提过,重构获得设计模式(Refactoring to Patterns).设计模式的应用不宜先入为主,一上来就使用设计模式是对设计模式的最大误用,在敏捷软件开发中,提倡使用的是通过重构来获得设计模式,这也是最好的使用设计模式的方法.
而关于重构的关键技法,包括了:
- 静态->动态
- 早绑定->晚绑定
- 继承->组合
- 编译时依赖->运行时依赖
- 紧耦合->松耦合
接下来我们来看看如何将一个程序,重构成模板方法模式.现代软件专业分工之后,也出现了"框架与应用程序的划分",框架实现人员先定义好框架的执行流程,也就是算法骨架(稳定),并提供可重写(overide)的接口(变化)给应用开发人员,以开发适应其应用程序的子步骤.模板方法通过晚绑定,实现了框架与应用程序之间的松耦合.
现在我们需要实现一个程序库,需要四个步骤来完成相应功能.其中step1,step3步骤稳定,而step2,step4则根据不同应用的具体需要,自行定义其具体功能,库开发人员无法预先实现好step2,step4.那么库开发人员可以先写好:
// 库开发人员
class Library{
public:
void step1(){
// 步骤1的具体实现
//...
}
void step3(){
//步骤3的具体实现
//...
}
应用程序开发人员则根据具体的应用需求,来实现剩余的两个步骤:
//应用程序开发人员
class Application{
public:
void step2(){
//步骤2的具体实现
//...
}
bool step4(){
//步骤4的具体实现
//...
}
}
然后应用程序开发人员还需要写一个main方法,将步骤以某种流程串起来:
//稳定
public static void main(String args[]) {
Library lib = new Library();
Application app = new Application();
lib.step1();
if (app.step2()) {
lib.step3();
}
app.step4();
}
这种办法实际上是一种C语言结构化的实现方式,虽然用的是C++,但没有体现出面向对象的特性来.main方法中,四个步骤的调用过程是相对稳定的,我们可以把这种稳定提升到库的实现中去,而应用程序开发人员,只需要实现"变化"的代码即可.这就引出了第二种做法.
第二种做法,是库开发人员不仅实现step1(),step3(),同时将step2(),step4()声明为纯虚函数,等待应用程序开发人员自己去实现这两个纯虚函数.注意到,main方法中定义的执行流程是相对稳定的,完全可以把这些步骤移动到库类中去.
//库开发人员
class Library{
public :
void run (){
step1();
if(step2()){ //支持变化-->虚函数的多态调用
step3();
}
step4(); //支持变化-->虚函数的多态调用
}
protected:
void step1(){ //稳定
//...
}
void step3(){ //稳定
//...
}
virtual bool step2() =0; //纯虚函数
virtual void step4() = 0; //纯虚函数
virtual ~Library(){
//...
}
};
注意step2,step4为纯虚函数,这是因为库开发人员无法知道怎么写,留给程序库开发人员来实现,也就是"把实现延迟",这在C++中体现为虚函数或纯虚函数,由应用程序开发人员继承Library以实现两个纯虚函数.这一段代码实际上体现了大部分设计模式应用的特点,也就是在稳定中包含着变化,run函数的算法流程是稳定的,但是算法的某个步骤是可变的,可变的延迟实现:
class Application: public Library{
protected:
virtual bool step2(){
//...子类重写实现
}
virtual void step4(){
//...子类重写实现
}
};
然后,应用程序开发人员,只需要通过多态指针来完成框架的使用:
public static void main(String args[]) {
Library * ptr = new Application();
ptr->run();
delete ptr;
}
指针ptr是一个多态指针,它声明为Library类型,实际指向的对象为Application类型对象.它会调用到基类的run方法,遇到step2,step4函数时,通过虚函数机制,调用到派生类实现的step2,step4函数.
回顾两种实现方式,我们可以发现,第一种实现方式中:
- 库开发者负责step1,step3 ;
- 应用程序开发者负责step2,step4,执行流程(稳定)
采用了模板方法模式的实现方式中:
- 库开发者负责step1,step3,执行流程(稳定)
- 应用程序开发者负责step2,step4
一般来说,框架/组件/库的实现,总是要先于应用程序的开发的.在第一种方式中,应用程序开发者(晚开发)的执行流程调用了库开发者定义好的函数(早开发),称为早绑定,而反过来在模板方法模式中,库开发者在执行流程中先调用了step2,step4函数,而这两个函数需要延迟到应用程序开发人员真正实现时,才通过虚函数机制进行调用,这种方式则称为早绑定.这便是重构使用设计模式的技法: 早绑定->晚绑定.
回过头来看看模板方法模式的定义:**定义一个操作中的算法的骨架,而将一些步骤延迟到子类中.Template Method使得子类刻意不改变一个算法的结构即可重定义该算法的某些特定步骤.(<<设计模式>> GoF) ** 所谓的骨架,要求是相对稳定的,在上面的例子中,如果step1,step3也是不稳定的,那么该情景下就不适用于适用设计模式,原因是软件体系中所有的东西都不稳定.设计模式的假设条件是必须有一个稳定点,如果没有稳定点,那么设计模式没有任何作用.反过来说,如果所有的步骤都是稳定的,这种极端情况也不适用于适用设计模式.设计模式总是处理"稳定中的变化"这种情景.设计模式最大的作用,是在稳定与变化之间寻找隔离点,然后来分离它们,从而来管理变化.从而我们也能够得到启发,学会分析出软件体系结构中哪部分是稳定的,哪部分是变化的,是学好设计模式的关键点.
再来看一看模板方法设计模式的结构:其中TemplateMethod() 方法也就是我们上面所说的run函数,它相对稳定,primitiveOperation1(),primitiveOperation2()为两个变化的函数,可由派生类实现,在TemplateMethod()中调用步骤.在下图中,红色圈为稳定的部分,而黑色圈为变化的部分.
在面向对象的时代,绝大多数的框架设计都使用了模板方法模式.作为一个应用程序开发人员,我们往往只需要实现几个步骤,框架便会把我们的步骤"串接"到执行流程中,有时候甚至连main函数都不用我们去实现.这样子也有弊端,我们看不见框架的执行流程,执行细节是怎么样的,往往有一种"只见树木不见森林"的感觉.
最后来总结以下模板方法设计模式.Template Method设计模式是一种非常基础性的设计模式,它要解决的问题是如何在确定稳定操作结构的前提下,来灵活应对各个子步骤的变化或者晚期实现需求.它使用了函数的多态性机制,为应用程序框架提供了灵活的拓展点,是代码复用方面的基本实现结构.Template Method设计模式明显划分了稳定与变化的关系,除了灵活应对子步骤的变化外,也是晚绑定的典型应用,通过反向控制结构,使得早期的代码可以调用晚期代码.而在具体实现上,被Template Method调用的虚函数,可以具有实现,也可以没有任何实现,这在C++中体现为虚函数或者纯虚函数,一般将这些函数设置为proteced方法.