我使用过一个简单的后台服务框架.这个框架上手很容易,我只须要继承一个基类,同时实现,或重写(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函数来运行程序:app
void main (){ //...准备工做 Service * myService = new MyService(); if( myService->run()){ //...后续处理 } // ... }
咱们的后台服务框架例子中,run函数定义了一个服务的稳定执行步骤,但某个步骤有着特定的需求没法立刻定义,须要延迟到派生类中去实现,这时候就须要采用模板方法模式.模板方法模式要解决的问题是:如何在肯定稳定操做结构的前提下,灵活地应对各个子步骤的变化或者晚期实现需求?框架
李建忠老师曾提过,重构得到设计模式(Refactoring to Patterns).设计模式的应用不宜先入为主,一上来就使用设计模式是对设计模式的最大误用,在敏捷软件开发中,提倡使用的是经过重构来得到设计模式,这也是最好的使用设计模式的方法.ide
而关于重构的关键技法,包括了:函数
接下来咱们来看看如何将一个程序,重构成模板方法模式.现代软件专业分工以后,也出现了"框架与应用程序的划分",框架实现人员先定义好框架的执行流程,也就是算法骨架(稳定),并提供可重写(overide)的接口(变化)给应用开发人员,以开发适应其应用程序的子步骤.模板方法经过晚绑定,实现了框架与应用程序之间的松耦合.设计
如今咱们须要实现一个程序库,须要四个步骤来完成相应功能.其中step1,step3步骤稳定,而step2,step4则根据不一样应用的具体须要,自行定义其具体功能,库开发人员没法预先实现好step2,step4.那么库开发人员能够先写好:指针
// 库开发人员 class Library{ public: void step1(){ // 步骤1的具体实现 //... } void step3(){ //步骤3的具体实现 //... }
应用程序开发人员则根据具体的应用需求,来实现剩余的两个步骤:code
//应用程序开发人员 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函数.
回顾两种实现方式,咱们能够发现,第一种实现方式中:
采用了模板方法模式的实现方式中:
通常来讲,框架/组件/库的实现,老是要先于应用程序的开发的.在第一种方式中,应用程序开发者(晚开发)的执行流程调用了库开发者定义好的函数(早开发),称为早绑定,而反过来在模板方法模式中,库开发者在执行流程中先调用了step2,step4函数,而这两个函数须要延迟到应用程序开发人员真正实现时,才经过虚函数机制进行调用,这种方式则称为早绑定.这即是重构使用设计模式的技法: 早绑定->晚绑定.
回过头来看看模板方法模式的定义:定义一个操做中的算法的骨架,而将一些步骤延迟到子类中.Template Method使得子类刻意不改变一个算法的结构便可重定义该算法的某些特定步骤.(< <设计模式> > GoF) 所谓的骨架,要求是相对稳定的,在上面的例子中,若是step1,step3也是不稳定的,那么该情景下就不适用于适用设计模式,缘由是软件体系中全部的东西都不稳定.设计模式的假设条件是必须有一个稳定点,若是没有稳定点,那么设计模式没有任何做用.反过来讲,若是全部的步骤都是稳定的,这种极端状况也不适用于适用设计模式.设计模式老是处理"稳定中的变化"这种情景.设计模式最大的做用,是在稳定与变化之间寻找隔离点,而后来分离它们,从而来管理变化.从而咱们也可以获得启发,学会分析出软件体系结构中哪部分是稳定的,哪部分是变化的,是学好设计模式的关键点.
再来看一看模板方法设计模式的结构:其中TemplateMethod() 方法也就是咱们上面所说的run函数,它相对稳定,primitiveOperation1(),primitiveOperation2()为两个变化的函数,可由派生类实现,在TemplateMethod()中调用步骤.在下图中,红色圈为稳定的部分,而黑色圈为变化的部分.
在面向对象的时代,绝大多数的框架设计都使用了模板方法模式.做为一个应用程序开发人员,咱们每每只须要实现几个步骤,框架便会把咱们的步骤"串接"到执行流程中,有时候甚至连main函数都不用咱们去实现.这样子也有弊端,咱们看不见框架的执行流程,执行细节是怎么样的,每每有一种"只见树木不见森林"的感受.
最后来总结如下模板方法设计模式.Template Method设计模式是一种很是基础性的设计模式,它要解决的问题是如何在肯定稳定操做结构的前提下,来灵活应对各个子步骤的变化或者晚期实现需求.它使用了函数的多态性机制,为应用程序框架提供了灵活的拓展点,是代码复用方面的基本实现结构.Template Method设计模式明显划分了稳定与变化的关系,除了灵活应对子步骤的变化外,也是晚绑定的典型应用,经过反向控制结构,使得早期的代码能够调用晚期代码.而在具体实现上,被Template Method调用的虚函数,能够具备实现,也能够没有任何实现,这在C++中体现为虚函数或者纯虚函数,通常将这些函数设置为proteced方法.