目录
一.专栏简介
本专栏是我学习《head first》设计模式的笔记。这本书中是用Java语言为基础的,我将用C++语言重写一遍,并且详细讲述其中的设计模式,涉及是什么,为什么,怎么做,自己的心得等等。希望阅读者在读完我的这个专题后,也能在开发中灵活且正确的使用,或者在面对面试官时,能够自信地说自己熟悉常用设计模式。
本章将开始模版方法模式的学习,还是以书中的星巴兹咖啡订单系统为例子。
二.是时候来更多的咖啡因了
茶和咖啡的制作方式非常类似:

三.快速来一些咖啡和茶的类
我们来编写一些创建咖啡和茶的代码:
templateMethod.h:
cpp
#pragma once
#include <iostream>
using namespace std;
class Coffee
{
public:
void prepareRecipe()
{
boilWater();
brewCoffeeGrinds();
pourInCup();
addSugarAndMilk();
}
void boilWater()
{
cout << "Boiling water" << endl;
}
void brewCoffeeGrinds()
{
cout << "Dripping Coffee through filter" << endl;
}
void pourInCup()
{
cout << "Pouring into cup" << endl;
}
void addSugarAndMilk()
{
cout << "Adding Sugar and Milk" << endl;
}
};
class Tea
{
public:
void prepareRecipe()
{
boilWater();
steepTeaBag();
pourInCup();
addLemon();
}
void boilWater()
{
cout << "Boiling water" << endl;
}
void steepTeaBag()
{
cout << "Steeping the tea" << endl;
}
void pourInCup()
{
cout << "Pouring into cup" << endl;
}
void addLemon()
{
cout << "Adding Lemon" << endl;
}
};
创建Coffee和Tea的类有代码重复,是一个我们需要清理设计的迹象。在这里,看起来我们应该把共性抽象到一个基类,因为咖啡和茶如此相似。
四.我们来抽象Coffee和Tea
抽象共性到基类的代码如下:
cpp
#pragma once
#include <iostream>
using namespace std;
class CaffeineBeverage
{
public:
virtual void prepareRecipe() = 0;
void boilWater()
{
cout << "Boiling water" << endl;
}
void pourInCup()
{
cout << "Pouring into cup" << endl;
}
};
class Coffee : public CaffeineBeverage
{
public:
void prepareRecipe() override
{
boilWater();
brewCoffeeGrinds();
pourInCup();
addSugarAndMilk();
}
void brewCoffeeGrinds()
{
cout << "Dripping Coffee through filter" << endl;
}
void addSugarAndMilk()
{
cout << "Adding Sugar and Milk" << endl;
}
};
class Tea : public CaffeineBeverage
{
public:
void prepareRecipe() override
{
boilWater();
steepTeaBag();
pourInCup();
addLemon();
}
void steepTeaBag()
{
cout << "Steeping the tea" << endl;
}
void addLemon()
{
cout << "Adding Lemon" << endl;
}
};
类图如下:

我们这次的改进可以,但忽略了一些Coffee和Tea的共性。
五.进一步设计
我们再看看咖啡和茶的冲泡法,发现两份冲泡法都遵循相同的算法:

那么,我们也能想办法抽象prepareRecipe(),代码如下:
cpp
#pragma once
#include <iostream>
using namespace std;
class CaffeineBeverage
{
public:
void prepareRecipe()
{
boilWater();
brew();
pourInCup();
addCondiments();
}
void boilWater()
{
cout << "Boiling water" << endl;
}
virtual void brew() = 0;
void pourInCup()
{
cout << "Pouring into cup" << endl;
}
virtual void addCondiments() = 0;
};
class Coffee : public CaffeineBeverage
{
public:
void brew() override
{
cout << "Dripping Coffee through filter" << endl;
}
void addCondiments() override
{
cout << "Adding Sugar and Milk" << endl;
}
};
class Tea : public CaffeineBeverage
{
public:
void brew() override
{
cout << "Steeping the tea" << endl;
}
void addCondiments() override
{
cout << "Adding Lemon" << endl;
}
};
类图如下:

上面的类我们做了什么:

六.认识模版方法
我们实现了模版方法模式。模版方法模式是什么,我们来看看CaffeineBeverage类的结构,它包含实际的"模版方法":

模版方法定义一个算法的步骤,允许子类提供一个或多个步骤的实现。
七.测试我们的代码并说明
测试代码:
cpp
#include "templateMethod.h"
int main()
{
Tea myTea;
myTea.prepareRecipe();
return 0;
}
运行结果:

现在我们来一步步泡茶并追踪模版方法如何运作。我们会看到模版方法控制算法。在算法的某一点,它让子类提供步骤的实现......
1.首先我们实例化一个Tea对象:
cpp
Tea myTea;
2.然后我们调用模版方法:
cpp
myTea.prepareRecipe();
3.首先我们烧水:
cpp
boilWater();
这发生在CaffeineBeverage中。
4.接下来,我们需要泡茶,只有子类知道如何做:
cpp
brew();
5.现在我们把茶倒进杯子;这一点所有饮料都一样,因此它发生在CaffeineBeverage中:
cpp
pourInCup();
6.最后,我们加调料,这对每种饮料来说是特定的,因此子类实现这一点:
cpp
addCondiments();
八.定义模版方法模式
模版方法模式在一个方法中定义一个算法的骨架,而把一些步骤延迟到子类。模版方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
这个模式是用来创建一个算法的模版。什么是模版?正如我们已经看到的,它就是一个方法,更具体地说,它是一个把算法定义为一组步骤的方法。这些步骤中的一个或多个被定义为抽象的,由子类实现。这保证了算法的结构保持不变,而子类提供某些部分的实现。
类图如下:

九.钩子
钩子是一个声明在抽象类中的方法,但它只给出空的或缺省的实现。这给了子类在变化点"挂钩进"算法的能力,如果需要,子类也有权忽略钩子。
下面是一种使用钩子的例子,代码如下:
templateMethod.h:
cpp
#pragma once
#include <iostream>
using namespace std;
class CaffeineBeverage
{
public:
void prepareRecipe()
{
boilWater();
brew();
pourInCup();
if(customerWantsCondiments()) addCondiments();
}
void boilWater()
{
cout << "Boiling water" << endl;
}
virtual void brew() = 0;
void pourInCup()
{
cout << "Pouring into cup" << endl;
}
virtual void addCondiments() = 0;
virtual bool customerWantsCondiments()
{
return true;
}
};
class Coffee : public CaffeineBeverage
{
public:
void brew() override
{
cout << "Dripping Coffee through filter" << endl;
}
void addCondiments() override
{
cout << "Adding Sugar and Milk" << endl;
}
};
class Tea : public CaffeineBeverage
{
public:
void brew() override
{
cout << "Steeping the tea" << endl;
}
void addCondiments() override
{
cout << "Adding Lemon" << endl;
}
bool customerWantsCondiments() override
{
string answer;
cout << "是否需要加调料(yes:加,no:不加):";
cin >> answer;
if (answer == "yes") return true;
else return false;
}
};
main.cpp:
cpp
#include "templateMethod.h"
int main()
{
Tea myTea;
myTea.prepareRecipe();
return 0;
}
运行结果如下:

或者

当算法的某部分可选时,使用钩子。
十.好莱坞原则
我们得到了另一个设计原则,称为好莱坞原则:

好莱坞HR恐怕经常这样说话哈哈哈。
好莱坞原则给我们一种防止"依赖腐烂"的方法。"依赖腐烂"的例子如高层组件依赖于低层组件,低层组件又依赖于高层组件,而高层组件又依赖于横向组件,横向组件又依赖于低层组件等。当腐烂蔓延时,没有人能轻易理解系统是怎么设计的。
有了好莱坞原则,我们允许低层组件把自己挂钩进系统,但由高层组件决定何时需要它们,以及怎样需要它们。换句话说,高层组件对低层组件的态度是"不要打电话给我,我们会打电话给你。"

十一.好莱坞原则和模版方法
好莱坞原则和模版方法模式之间的关系可能还是相当明显的:当我们用模版方法模式设计时,我们在告诉子类,"不要打电话给我,我们会打电话给你。"怎么做?我们再看看CaffeineBeverage的设计:

超类主导演出,当它们需要的时候,会调用子类的方法,这就跟好莱坞一样。
我们可以轻易得知工厂方法和观察者也采用了好莱坞原则。
十二.总结
工厂方法模式是模版方法的一个特例。在这个特例中,原语操作用来创建并返回对象。
下面是一些总结:
- 模版方法定义了算法的步骤,把这些步骤的实现延迟到子类。
- 模版方法模式为我们提供了一种代码复用的重要技巧。
- 模版方法的抽象类可以定义具体方法、纯虚方法和钩子(虚方法)。
- 纯虚方法由子类实现。
- 钩子是一种虚方法,它在抽象类中不做事或者只做缺省的事情,但子类可以覆盖它。
- 好莱坞原则告诉我们,把决策权放在高层模块中,以便决定如何以及何时调用低层模块。
- 策略模式和模版方法模式都封装算法,前者通过组合,后者通过继承。
- 工厂方法是模版方法的一个特例。