设计模式-模板方法模式
案例分析
咖啡与茶冲泡
咖啡冲泡法:
- 把水煮沸
- 用沸水冲泡咖啡
- 把咖啡倒进杯子
- 加糖和牛奶
茶冲泡法:
- 把水煮沸
- 用沸水浸泡茶叶
- 把茶倒进杯子
- 加柠檬
代码实现
public class Coffee{
//这是咖啡冲泡法,直接取自上面的冲泡步骤
void prepareRecipe(){
//以下每个步骤都被实现在分离的方法中
boilWater();
brewCoffeeGrinds();
pourInCup();
addSugarAndMilk();
}
//每个方法都实现算法中的一个步骤,如煮水,冲泡、倒进杯子,加糖和牛奶
public void boilWater(){
System.out.println("Boiling water");
}
public void brewCoffeeGrinds(){
System.out.println("Dripping Coffee through filter");
}
public void pourInCup(){
System.out.println("Pouring into cup");
}
public void addSugarAndMilk(){
System.out.println("Adding Sugar and Milk");
}
}
public class Tea{
//与上述咖啡实现很像,其中第2个和第4个步骤不一样,但基本上是相同的冲泡法
void prepareRecipe(){
boilWater();
steepTeaBag();
pourInCup();
addLemon();
}
//重复代码,与上述冲泡咖啡相同
public void boilWater(){
System.out.println("Boiling water");
}
//冲泡茶专有的方法
public void steepTeaBag(){
System.out.println("Steeping the tea");
}
//重复代码,与上述冲泡咖啡相同
public void pourInCup(){
System.out.println("Pouring into cup");
}
//冲泡茶专有的方法
public void addLemon(){
System.out.println("Adding Lemon");
}
}
在实现中,我们发现了重复代码,需要将共同部分抽取出来,放进一个基类中。
抽象类图
这个设计如何?是否还忽略了其他共同点?咖啡和茶之间还有什么是相似的?
重新设计
共同点分析
两份冲泡法都采用了相同的算法:
- 把水煮沸
- 用热水冲泡茶或者咖啡
- 把饮料倒进杯子
- 在饮料内加入适当的调料
其中步骤1和3已经被抽出来,放到基类中了。但是2和4没有被抽出来,但是他们是一样的,只是应用在不同的饮料上。
那么,我们有办法将prepareRecipe()也抽象化吗?
抽象prepareRecipe()
现在,我们从每一个子类中(咖啡或者茶)中逐步抽象prepareRecipe()
- 抽象第一个问题,就是咖啡使用brewCoffeeGrinds()和addSugarAndMilk()方法,而茶使用steepTeaBag()和addLemon()方法。
浸泡(steep)茶和冲泡(brew)咖啡,差异其实不大,我们给一个新的方法名称,比如说brew(),加糖和牛奶和加柠檬也很相似,都是在加入调料,我们也给一个新的方法名称,比如addCondiments(),所以新的prepareRecipe()方法如下:
void prepareRecipe(){
boilWater();
brew();
pourInCup();
addCondiments();
}
- 有了新的prepareRecipe()方法,从超类CaffeineBeverage开始
public abstract class CaffeineBeverage{
//设计为final,因为我们不希望子类覆盖这个方法,我们将步骤2和4抽象为brew()和addCondiments()方法
final void prepareRecipe(){
boilWater();
brew();
pourInCup();
addCondiments();
}
//因为咖啡和茶处理这些方法的做法不同,所以这两个方法声明为抽象,具体交给子类去实现
abstract void brew();
abstract void addCondiments();
void boilWater(){
System.out.println("Boiling water");
}
void pourInCup(){
System.out.println("Pouring into cup");
}
}
public class Coffee extends CaffeineBeverage{
@Override
public void brew(){
System.out.println("Dripping Coffee through filter");
}
@Override
public void addCondiments(){
System.out.println("Adding Sugar and Milk");
}
}
public class Tea extends CaffeineBeverage{
@Override
public void brew(){
System.out.println("Steeping the tea");
}
@Override
public void addCondiments(){
System.out.println("Adding Lemon");
}
}
设计类图
认识模板方法
上述实现的就是模板方法模式,来看一下代码结构,它包含了“模板方法”
public abstract class CaffeineBeverage{
//prepareRecipe()是我们的模板方法,为什么?
//因为:
//1.它用作一个算法的模板,在这个例子中,算法是用来制作咖啡因饮料
//2.在这个模板中,算法内的每一个步骤都被一个方法代表了。
//3.某些方法是由这个类(也就是超类)处理的
//4.某些方法则是由子类处理的
//5.需要由子类提供的方法必须在超类中声明为抽象
final void prepareRecipe(){
boilWater();
brew();
pourInCup();
addCondiments();
}
abstract void brew();
abstract void addCondiments();
void boilWater(){
//实现
}
void pourInCup(){
//实现
}
}
模板方法定义了一个算法的步骤,并允许子类为一个或多个步骤提供实现。
模板方法带我我们什么?
模板方法提供的酷炫咖啡因饮料:
- 由CaffeineBeverage类主导一切,它拥有算法,而且保护这个算法
- 对子类来说,CaffeineBeverage类的存在,可以将代码的复用最大化
- 算法只存在于一个地方,所以容易修改
- 这个模板方法提供了一个框架,可以让其他的咖啡因饮料插进来,新的饮料只需要实现自己的方法就可以了
- CaffeineBeverage类专注在算法本身,而由子类提供完整的实现
定义模板方法模式
模板方法模式在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
这个模式是用来创建一个算法的模板。什么事模板?模板就是一个方法。更具体地说,这个方法将算法定义成一组步骤,其中任何步骤都可以是抽象的,由子类负责实现。这可以确保算法的结构保持不变,同时由子类提供部分实现。
模板方法模式类图
对模板方法进行挂钩
abstract class AbstractClass{
final void templateMethod(){
primitiveOperation1();
primitiveOperation2();
//假设这个方法是新加入的方法,改变了模板方法
concreateOperation();
hook();
}
//定义抽象,由具体的类实现
abstract void primitiveOperation1();
abstract void primitiveOperation2();
//这个具体的方法被定义在抽象类中,将它声明为final,这样一来子类就无法覆盖它。它可以被模板方法直接使用,或者被子类使用。
final void concreateOperation(){
//这里是实现
}
//我们也可以有“默认不做事的方法”,我们称这种方法为“hook”(钩子)。子类可以视情况决定要不要覆盖它们。
void hook(){}
}
钩子是一种被声明在抽象类中的方法,但只有空的或者默认的实现。钩子的存在,可以让子类有能力对算法的不同点进行挂钩。要不要挂钩,由子类自行决定。
我们来看一下钩子的用途:
public abstract class CaffeineBeverageWithHook{
void prepareRecipe(){
boilWater();
brew();
pourInCup();
//加上一个小的条件语句,该条件是否成立,是由一个具体方法customerWantsCondiments()决定的,如果顾客“想要”调料,只有这时我们才调用addCondiments()
if(customerWantsCondiments()){
addCondiments();
}
}
abstract void brew();
abstract void addCondiments();
void boilWater(){
System.out.println("Boiling water");
}
void pourInCup(){
System.out.println("Pouring into cup");
}
//我们在这里定义了一个方法,(通常)是空的缺省实现。这个方法只会返回true,不做别的事。
//这就是一个钩子,子类可以覆盖这个方法,但不见得一定这么做。
boolean customerWantsCondiments(){
return true;
}
}
public class CoffeeWithHook extends CaffeineBeverageWithHook{
public void brew(){
System.out.println("Dripping Coffee through filter");
}
public void addCondiments(){
System.out.println("Adding Sugar and Milk");
}
//覆盖钩子,提供了自己的功能
@Override
public boolean customerWantsCondiments(){
//让用户输入他们对调料的决定,根据用户的输入返回true或者false
String answer = getUserInput();
if(ansser.toLowerCase().startsWith("y")){
return true;
}else{
return false;
}
}
private String getUserInput(){
String answer = null;
System.out.println("Would you like milk and sugar with your coffee (y/n)?");
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
try{
answer = in.readLine();
}catch(IOException ioe){
System.error.println("System.in");
}
if(answer == null){
return "no";
}
return answer;
}
}
模板方法模式使用场景
当我创建一个模板方法时,怎么才能知道什么时候该使用抽象方法,什么时候使用钩子呢?
- 当你的子类“必须”提供算法中某个方法或步骤的实现时,就使用抽象方法
- 如果算法的这个部分是可选的,就使用钩子
- 如果是钩子的话,子类可以选择实现这个钩子,但并不强制这么做
模板方法模式应用
模板方法排序
java数组类的设计者提供给我们一个方便的模板方法用来排序。
- mergeSort方法包含排序算法,此算法依赖于compareTo()方法的实现来完成算法,可以把该方法想象成一个模板方法。
- 我们需要实现compareTo()方法,“填补”模板方法的缺憾。
- sort()方法希望能够用于所有的数组,所以他们定义了一个静态方法,而由被排序的对象内的每个元素自行提供比较大小的算法部分。
- 由于不需要继承数组就可以使用这个算法,这样使得排序变得更有弹性、更有用
- 虽然这不是教科书的模板方法,但它的实现仍然符合模板方法模式的精神
总结
- “模板方法”定义了算法的步骤,把这些步骤的实现延迟到子类
- 模板方法模式为我们提供了一种代码复用的重要技巧
- 模板方法的抽象类可以定义具体方法、抽象方法和钩子
- 抽象方法由子类实现
- 钩子是一种方法,它在抽象类中不做事,或者只做默认的事情,子类可以选择要不要去覆盖它
- 为了防止子类改变模板方法中的算法,可以将模板方法声明为final
- 好莱坞原则告诉我们,将决策权放在高层模块中,以便决定如何以及何时调用低层模块
- 策略模式和模板方法模式都封装算法,一个用组合,一个用继承
- 工厂方法是模板方法的一种特殊版本