您的位置:首页 > 编程语言 > C语言/C++

设计模式系列(十二)模板方法模式(Template Method Pattern)

2015-12-13 16:41 686 查看

设计模式系列(十二)模板方法模式(Template Method Pattern)

    模板方法模式是在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。

    在最开始先来说一个和模板方法模式关系比较紧密的原则:“好莱坞原则”,即别调用我们,我们会调用你。形象的来说就是,别打电话给我,在我需要你的时候会打电话给你。

    举个例子来说,通常打电话的过程都是一样的:拨号---呼叫---等待接听---通话---通话结束,这个过程我们就可以定义成一个算法骨架,这个算法骨架由基类来实现,然后在保持这个骨架不变的情况下,子类可以实现算法中的某些步骤,比如说怎么拨号,怎么接听,不同的手机方式也有些不一样,然后不同的手机就定义了不同的具体算法,但是算法骨架还是一样的,只是实现形式不一样而已。所以,可以看出来模板方法模式就是在基类中定义一个方法(C/C++中一般称为函数),然后这个方法中调用了整个算法流程的所有函数,但是这些函数并不一定都在基类中实现了,有些方法定义成抽象的,由子类来实现,有些方法只定义默认的实现,由子类挂钩实现,然后不同的子类相当于实现了不同的具体方法,但是算法骨架还是一样的。

    模板方法模式中主要的角色有:

(1)抽象角色(AbstractClass):抽象角色一般是一个抽象类,该类中可以定义很多方法,可以是具体的或者抽象的,在该类中最重要的就是“模板方法”,在C/C++中可以称之为一个模板函数,这个函数负责调用所有算法流程中的函数,构成一个算法框架,但是其中调用的函数有些可能是延迟到子类才实现;

(2)具体角色(ConcreteClass):具体角色是具体类,继承自抽象角色,实现了抽象角色中的抽象方法,构成一个完整的算法,相当于把算法框架中空缺的部分填充进去,所以可以有很多很多的具体角色,都继承自抽象角色,从而形成不同的算法。

    模板方法模式的实现中,抽象角色里面通常有两大类的方法:

(1)模板方法:这个方法就是用来调用其他的方法从而形成算法的框架,所以这个方法是核心;

(2)基本方法:一般包括三种基本方法,即抽象方法、具体方法和钩子方法。

    第一,抽象方法就是一个只提供声明的方法,在抽象类中没有任何实现,留给子类实现,在C++中用virtual和=0来实现,下面例子中会看到;

    第二,具体方法就是在抽象类中直接给出实现的方法,子类可以选择覆盖这些方法,但是通常可以声明为final,从而不让子类覆盖,因为这些方法通常是算法中固定的步骤,是框架中确定的部分,所以在抽象类中直接实现;

    第三,钩子方法比较特殊,一般都是在抽象类中给出一个空的钩子方法或者默认实现的钩子方法,然后让子类去覆盖这个方法,从而子类可以修改算法的一些实现,形象来说就是抽象类中留出了一个钩子,这个钩子放在那里,子类可以选择将具体的实现挂在这个钩子上,每次执行算法,都会去执行这个钩子,但是钩子上是否有东西以及是什么东西由子类决定,即抽象类给出钩子,子类去挂钩。更具体来说就是在生产的抽象流水线上给出一个钩子,然后具体的流水线可以挂上不同的肉类,从而生产出不同的食品,这些食品可以选择带肉,那么生产线就在钩子上挂上肉,这些食品也可以不带肉,那么生产线就让钩子空着。

    使用钩子的主要目的在于:(1)钩子可以让子类实现算法中可选的部分,或者在钩子对于子类的实现并不重要的时候,子类可以对此钩子置之不理;(2)钩子也可以让子类能够有机会对模板方法中某些即将发生或刚刚发生的步骤作出反应;(3)钩子也可以让子类有能力为抽象作一些决定。

    总结来说,抽象方法在抽象类中给出留给具体类实现;具体方法直接在抽象类中实现,尽量不允许具体类覆盖;钩子方法在抽象类中给出空实现或者默认实现,然后具体类可以选择覆盖或者不搭理这个方法,即挂钩或者不挂钩,钩子方法的引入使得子类可以控制父类的行为。

    那么我们什么时候选择使用抽象方法,什么时候选择使用钩子呢?一般来说当子类必须提供算法中某个方法或者步骤的实现时,就使用抽象方法。如果算法的这个部分是可选的,就用钩子。对于钩子来说,子类可以选择实现,但是并不强制这么做。

    模板方法模式主要适用于:

(1)算法框架实现一个算法的不变的部分,并将可变的行为留给子类来实现;

(2)各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复;

(3)控制子类扩展,模板方法只在特定点调用“Hook”操作,这样就只允许在这些点进行扩展;

(4)在模板方法模式中,由于面向对象的多态性,子类对象在运行时将覆盖父类对象,子类中定义的方法也将覆盖父类中定义的方法,因此程序在运行时,具体子类的基本方法将覆盖父类中定义的基本方法,子类的钩子方法也将覆盖父类的钩子方法,从而可以通过在子类中实现的钩子方法对父类方法的执行进行约束,实现子类对父类行为的反向控制。

    模板方法模式的优点有:

(1)在父类中形式化地定义一个算法,而由它的子类来实现细节的处理,在子类实现详细的处理算法时并不会改变算法中步骤的执行次序。

(2)模板方法模式是一种代码复用技术,它在类库设计中尤为重要,它提取了类库中的公共行为,将公共行为放在父类中,而通过其子类来实现不同的行为,它鼓励我们恰当使用继承来实现代码复用。

(3)可实现一种反向控制结构,通过子类覆盖父类的钩子方法来决定某一特定步骤是否需要执行。

(4)在模板方法模式中可以通过子类来覆盖父类的基本方法,不同的子类可以提供基本方法的不同实现,更换和增加新的子类很方便,符合单一职责原则和开闭原则,也符合好莱坞原则。

    模板方法模式的缺点有:

(1)需要为每一个基本方法的不同实现提供一个子类,如果父类中可变的基本方法太多,将会导致类的个数增加,系统更加庞大,设计也更加抽象,此时,可结合桥接模式来进行设计。

(2)由于模板方法模式使用了继承方式,所以在一定程度上并不符合多用组合,少用继承的设计原则,不过这个并不影响其使用。

    下面来看一个例子,该例主要由三个文件组成,依次是:TemplateMethodPattern.h、TemplateMethodPattern.cpp、TemplateMethodPatternTest.cpp。

// 模板方法模式
// TemplateMethodPattern.h文件

#ifndef TEMPLATEMETHOD
#define TEMPLATEMETHOD

#include <iostream>
#include <iomanip>
#include <string>
#include <vector>

using std::string;
using std::cout;
using std::cin;
using std::endl;
using std::vector;

// 抽象类
class CaffeineBeverageWithHook
{
public:
// 模板方法,即模板函数
void prepareRecipe();

// 具体的算法函数,由该类实现
void boilWater();
void pourInCup();

// 抽象的虚算法函数,由子类实现
virtual void brew() = 0;
virtual void addCondiments() = 0;

// Hook函数,即钩子函数,由子类挂钩实现,该类提供默认实现
virtual bool customerWantsCondiments();
};

// 具体类
class TeaWithHook : public CaffeineBeverageWithHook
{
public:
void brew();
void addCondiments();

// Hook函数,即钩子函数,子类进行挂钩实现
bool customerWantsCondiments();
};

class CoffeeWithHook : public CaffeineBeverageWithHook
{
public:
void brew();
void addCondiments();

// Hook函数,即钩子函数,子类进行挂钩实现
bool customerWantsCondiments();
};

#endif

// TemplateMethodPattern.cpp文件

#include "TemplateMethodPattern.h"

// 抽象类CaffeineBeverageWithHook
// 模板方法,即模板函数
void CaffeineBeverageWithHook::prepareRecipe()
{
// 定义了一个算法的所有执行过程
boilWater();
brew();
pourInCup();

if (customerWantsCondiments())
{
addCondiments();
}
}

// 具体的算法函数,由该类实现
void CaffeineBeverageWithHook::boilWater()
{
cout << "Boiling water" << endl;
}

void CaffeineBeverageWithHook::pourInCup()
{
cout << "Pouring into cup" << endl;
}
// Hook函数,即钩子函数,由子类挂钩实现,该类提供默认实现
bool CaffeineBeverageWithHook::customerWantsCondiments()
{
return true;
}

// 具体类
void TeaWithHook::brew()
{
cout << "Steeping the tea" << endl;
}

void TeaWithHook::addCondiments()
{
cout << "Adding Lemon" << endl;
}

// Hook函数,即钩子函数,子类进行挂钩实现
bool TeaWithHook::customerWantsCondiments()
{
cout << "Would you like milk and sugar with your coffee(y / n) ? " << endl;

string in;
cin >> in;

if (!in.compare("y"))
{
return true;
}
else
{
return false;
}
}

void CoffeeWithHook::brew()
{
cout << "Dripping Coffee through filter" << endl;
}

void CoffeeWithHook::addCondiments()
{
cout << "Adding Sugar and Milk" << endl;
}

// Hook函数,即钩子函数,子类进行挂钩实现
bool CoffeeWithHook::customerWantsCondiments()
{
cout << "Would you like milk and sugar with your coffee(y / n) ? " << endl;

string in;
cin >> in;

if (!in.compare("y"))
{
return true;
}
else
{
return false;
}
}


// TemplateMethodPatternTest.cpp文件

#include "TemplateMethodPattern.h"

void main()
{
TeaWithHook teaHook;
CoffeeWithHook coffeeHook;

cout << "------------------------------------------" << endl;
cout << "准备茶" << endl;
teaHook.prepareRecipe();

cout << "------------------------------------------" << endl;
cout << "准备咖啡" << endl;
coffeeHook.prepareRecipe();

cout << "------------------------------------------" << endl;
}


    该例的运行结果如图1所示,UML类图如图2所示。


    
图1 运行结果



图2 UML类图

    该例就是一种代码复用和算法抽象的例子。假设做茶和做咖啡的流程基本一样,然后将这些流程抽象出来得到一个抽象类,其中包含了所有的流程。CaffeineBeverageWithHook类是抽象角色,其中的prepareRecipe()函数是模板方法,是算法的核心,其中调用了所有流程函数,customerWantsCondiments()是一个钩子函数,给出了默认实现。TeaWithHook类和CoffeeWithHook类是具体角色,其中实现了抽象方法,覆盖了钩子方法,两个类实现了不同的算法,但是整个算法的流程还是一样的。当我们需要新的算法时,不需要修改用户代码,也不需要修改抽象类,只需要增加一些具体类或者修改一些具体类即可,这就是开闭原则的体现,至于好莱坞原则,就是由高层的抽象类来负责管理算法的流程,调用所有的算法函数,而子类则不需要管自己怎么被调用。

    前面讲过的工厂方法模式是模板方法模式的一种特殊情况。大家回想下,策略模式是封装了一族算法,是不是听起来和模板方法模式有些类似,那么它们之间有什么区别呢?

(1)策略模式封装算法,但是使用组合方式,而模板方法模式使用继承方式;

(2)策略模式是使用对象组合的方法让客户可以选择算法的具体实现,更加有弹性,而模板方法模式是直接定义了算法的大纲,减少代码重复,有较高的代码复用率,效率比较高,不过其依赖程度也高于策略模式。

    总之,模板方法模式是一种比较简单的设计模式,但是十分有用,在swing框架、applet框架、java.io中都可以见到,大家可以按照设计需求进行选择。

参考资料:
http://blog.csdn.net/lcl_data/article/details/9199961 http://blog.csdn.net/lovelion/article/details/8299927
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息