设计模式——模板方法模式
模板方法模式
1、定义
模板方法模式 在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
这个定义还是比较好理解的,由父类定义定义一个模板,这里的模板就是一个方法,它将算法定义成一组步骤,其中任何步骤都可以是抽象的,由子类负责实现。通过模板方法模式,可以确保算法的结构保持不变,同时由子类提供部分实现。下面我将通过具体实例来讲解一下模板方法模式。
2、模板方法模式
2.1、需求
假设我们需要模拟饮品店准备饮品的过程,例如在准备咖啡时,分为将水煮沸、用沸水冲泡咖啡、将咖啡倒入杯子、添加糖和牛奶四个步骤;准备茶时,分为将水煮沸、用沸水浸泡茶叶、把茶倒入杯子、添加柠檬四个步骤。
2.2、第一种实现方式
第一眼看到需求,我们很容易就可以想到,要模拟准备饮品的过程,我们只需要针对每种饮品定义一个类,在每个类中编写准备的过程即可,所以我们可以分别定义咖啡类Coffee和茶类Tea,它们的实现方式如下:
/**咖啡类*/
public class Coffee {
public void prepareRecipe() {
boilWater();
brewCoffeeGrinds();
pourInCup();
addSugarAndMilk();
}
public void boilWater() {
System.out.println("把水煮沸");
}
public void brewCoffeeGrinds() {
System.out.println("用沸水冲泡咖啡");
}
public void pourInCup() {
System.out.println("把咖啡倒进杯子");
}
public void addSugarAndMilk() {
System.out.println("加糖和牛奶");
}
}
//-------------------------------------------
/**茶类*/
public class Tea {
public void prepareRecipe() {
boilWater();
steepTeaBag();
pourInCup();
addLemon();
}
public void boilWater() {
System.out.println("把水煮沸");
}
public void steepTeaBag() {
System.out.println("用沸水浸泡茶叶");
}
public void pourInCup() {
System.out.println("把茶倒进杯子");
}
public void addLemon() {
System.out.println("加柠檬");
}
}
在这两个类中我们分别定义了一个准备饮品的方法,这个方法中包含了准备饮品的每个步骤,并各自实现了所有的步骤,按照这个思路,以后如果有新的饮品出现,我们只需要定义新的饮品类即可。但是仔细观察我们定义的两个类可以发现,这两个类都有准备饮品的方法,我们是否可以把它抽象出来?另外,准备饮品的步骤中,咖啡和茶的第一步均为“将水煮沸”、第三步均为“将XX倒进杯子”,这两个步骤是一样的,我们是否也能将它们抽出来呢?
2.3、改进后的实现方式
依照面向对象的编程思想,我们可以将饮品类抽象出一个父类,这个父类包含一个准备饮品的抽象方法(因为所有饮品都必须实现这个方法,但是实现方式不一定相同)和所有饮品的公共方法,我们可以按照如下的方式进行定义:
/**所有咖啡因饮料的公共父类*/
public abstract class CaffeineBeverage {
public abstract void prepareRecipe();
public void boilWater() {
System.out.println("把水煮沸");
}
public void pourInCup() {
System.out.println("倒进杯子");
}
}
有了这个父类之后我们可以定义它的子类Coffee和Tea:
/**咖啡类*/
public class Coffee extends CaffeineBeverage{
public void prepareRecipe() {
boilWater();
brewCoffeeGrinds();
pourInCup();
addSugarAndMilk();
}
public void brewCoffeeGrinds() {
System.out.println("用沸水冲泡咖啡");
}
public void addSugarAndMilk() {
System.out.println("加糖和牛奶");
}
}
//-------------------------------------------
/**茶类*/
public class Tea extends CaffeineBeverage{
public void prepareRecipe() {
boilWater();
steepTeaBag();
pourInCup();
addLemon();
}
public void steepTeaBag() {
System.out.println("用沸水浸泡茶叶");
}
public void addLemon() {
System.out.println("加柠檬");
}
}
这两个类分别实现了准备饮品方法,同时还自己定义了两个独有的方法,在准备饮品方法中,它们同时调用了父类和子类的步骤。
通过抽象和继承能够我们很好的实现需求,但是有没有办法再进行优化呢?我们继续观察咖啡类和茶类,这两个类自行定义了两个方法,这两个方法是相互不同的,但是通过观察我们发现,冲泡咖啡和浸泡茶叶是不是可以合并成一个方法,例如加入原料这样的方法呢?最后一个方法是加糖和牛奶或者加柠檬,是否也可以合并成一个方法,例如加入辅料这样的方法呢?这样我们可以将准备饮品的所有的步骤都抽象到父类中了,父类可以直接定义一个不可变的准备饮品方法,在这个方法中定义一系列的步骤,子类无需实现这个方法,需要准备饮品时直接调用父类的方法即可,但是子类可以自行实现其中的一些步骤,这样可以满足自己的特殊需求。我们可以通过模板方法模式来实现需求。
2.4、模板方法模式的实现方式
我们修改父类的定义:
public abstract class CaffeineBeverage {
public final void prepareRecipe() {
boilWater();
brew();
pourInCup();
if(customerWantsCondiments()) {
addCondiments();
}
}
public void boilWater() {
System.out.println("把水煮沸");
}
public void pourInCup() {
System.out.println("倒进杯子");
}
public abstract void brew();
public abstract void addCondiments();
public boolean customerWantsCondiments() {
return true;
}
}
我们先看一下这个类的定义,我们首先定义了一个准备饮品的方法prepareRecipe(),这个方法中调用了准备饮品的一系列步骤,同时我们将prepareRecipe()方法定义为final,这样子类就不能覆盖这个方法了。然后我们在父类中定义了所有准备饮品的步骤,这些步骤有些是已经实现了的,有些步骤未实现,未实现的步骤可由子类自行实现。另外,我们还定义了一个方法customerWantsCondiments(),这个方法返回的是一个布尔值,在这个类中我们将这个方法的返回定义为恒定的值true,这个方法的使用我们会在后续进行详细说明。
接下来我们定义咖啡类和茶类:
/**咖啡类*/
public class Coffee extends CaffeineBeverage {
@Override
public void brew() {
System.out.println("用沸水冲泡咖啡");
}
@Override
public void addCondiments() {
System.out.println("加糖和牛奶");
}
public boolean customerWantsCondiments() {
String answer = getUserInput();
if(answer.toLowerCase().startsWith("y")) {
return true;
} else {
return false;
}
}
private String getUserInput() {
String answer = null;
System.out.println("请问是否需要添加糖和牛奶?(y/n)");
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
try {
answer = in.readLine();
} catch (IOException e) {
e.printStackTrace();
}
if(answer == null) {
return "no";
}
return answer;
}
}
//---------------------------------
/**茶类*/
public class Tea extends CaffeineBeverage{
@Override
public void brew() {
System.out.println("用沸水浸泡茶叶");
}
@Override
public void addCondiments() {
System.out.println("加柠檬");
}
}
可以看到,咖啡类和茶类都实现了brew()方法和addCondiments()方法,但是咖啡类还重写了customerWantsCondiments()方法,这个方法用于根据用户输入判断是否需要添加辅料,根据父类中的步骤定义,只有用户需要添加辅料的时候才会调用添加辅料方法,但是茶类中并没有重写这个方法,也就是说,茶类使用的是父类定义的customerWantsCondiments()方法,也就是永远返回true,这样一来,茶类在准备饮品的时候会一直添加柠檬。在模板方法模式中,customerWantsCondiments()这样的方法被称为钩子,它在父类定义中通常什么都不做,或者有一个默认的实现,子类可以覆盖这个方法,也可以不覆盖。
在模板方法模式中,我们将需要子类实现的步骤定义为抽象方法,钩子也是需要子类实现的,那么我们什么时候使用抽象方法什么时候使用钩子呢?当子类必须要实现算法中某个步骤的时候,就可以使用抽象方法,当某个步骤是可选的,那么就可以使用钩子。
那么我们为什么要使用钩子呢?钩子可以让子类实现算法中的可选部分,同时钩子能让子类有机会对模板方法中某些即将发生的(或刚刚发生的)步骤做出反应,另外,钩子也让子类有能力为其抽象类做出一些决定,就像我们上面示例中,我们通过钩子来让用户决定是否需要添加辅料。
最后,我们来看一下模板方法模式的类图(类图摘自《Head First设计模式》):
父类中包含了一个模板方法,这个模板方法实现了算法的过程,但是算法的步骤父类并没有全部实现,可以由子类来实现一个或多个步骤。
3、总结
模板方法模式定义了算法的步骤,并将这些步骤的实现延迟到了子类,它为我们提供了一个代码复用的重要技巧。在模板方法模式的抽象类中,我们可以定义具体方法、抽象方法和钩子,其中抽象方法需要由子类实现,子类可以选择修改或者不修改钩子。模板方法模式在实际中应用很多,而且很多时候我们会遇到模板方法的变体,这就需要我们自己有能力进行分析辨别。