在模板(Template Pattern)中,一个抽象类公开定义了执行它的方式/模板,它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行,这种类型的设计模式属于行为型设计模式。今天我们就来一起学习探究下设计模式中的模板方法模式,下面是我们今天要学习的内容大纲
意图:定义一个操作中的算法骨架,而将一些步骤延迟到子类中,模板方法使得子类可以不改变一个算法的结构即可重新定义该算法的某些特定步骤
主要解决:一些方法通用,却在每一个子类都重写了这一方法。
何时使用:有一些通用的方法。
如何解决:将这些通用算法抽象出来。
关键代码:在抽象类实现,其它步骤在子类实现。
使用场景:1)多个子类有共有的方法,并且逻辑基本相同时
2)重要复杂的算法,可以把核心算法设计为模板方法
3)重构时模板方法模式是一个经常使用的模式
注意事项:为防止恶意操作,一般模板方法都加上 final 关键字。
这里我们通过在一家饮料店喝咖啡喝茶来慢慢进行演绎,在饮料店里,茶和咖啡的冲泡方式非常相似,如下:
咖啡冲泡法:
1)把水煮沸
2)用沸水冲泡咖啡
3)把咖啡倒进杯子
4)加糖和牛奶
茶冲泡法:
1)把水煮沸
2)用沸水冲泡茶
3)把茶倒进杯子
4)加柠檬
从上面的步骤我们可以看到,咖啡喝茶的冲泡方法大致上是一样的
接下来我们来搞定咖啡和茶的类:
咖啡类:
/**
* 咖啡类
*
* @author qiudengjiao
*/
public class Coffee {
/**
* 这里实现了算法中的一个步骤,每个步骤都被实现在分离的方法中
*/
void prepareRecipe() {
builWater();
brewCoffeeGrinds();
pourInCup();
addSugarAndMilk();
}
/**
* 煮沸水
*/
public void builWater() {
System.out.println("煮沸水");
}
/**
* 冲泡咖啡
*/
public void brewCoffeeGrinds() {
System.out.println("冲泡咖啡");
}
/**
* 把咖啡倒进杯子
*/
public void pourInCup() {
System.out.println("把咖啡倒进杯子");
}
/**
* 加糖和奶
*/
public void addSugarAndMilk() {
System.out.println("加糖和奶");
}
}
茶类:
/**
* 茶类
*
* @author qiudengjiao
*/
public class Tea {
/**
* 这里实现了算法中的一个步骤,每个步骤都被实现在分离的方法中
*/
void prepareRecipe() {
builWater();
steepTeaBag();
pourInCup();
addLemon();
}
/**
* 煮沸水
*/
public void builWater() {
}
/**
* 冲泡茶
*/
public void steepTeaBag() {
}
/**
* 把茶倒进杯子
*/
public void pourInCup() {
}
/**
* 加柠檬
*/
public void addLemon() {
}
}
从上面我们可以看到,泡茶和咖啡的步骤很像,其中第二和第四步骤不一样,第一和第三步都是一样的,也就是说这里出现了重复代码,发现了重复代码,这是好现象,表示我们需要清理一下设计,在这里,既然茶和咖啡是如此的相似,所以我们应该将共同的部分抽取出来,放进一个基类中。
接下来我们来进行更进一步的设计
1)我们遇到的第一个问题,就是咖啡使用 brewCoffeeGrinds() 和 addSugarAndMilk() 方法,而茶使用 steepTeaBag() 和 addLemon() 方法。
这里由于第二步和第第四步不太一样,所以我们给他们一个新的名称,不管是泡茶还是泡咖啡我们都用 brew() 这个方法来统一,类似的,加糖和牛奶和加柠檬很相似,都是在饮料中加入调料,我们也给它一个新的方法名称来解决这个问题,就叫做 addCondiments() 吧,这样一来新的 prepareRecipe() 方法就变成了如下的样子:
2)现在我们有了新的 prepareRecipe() 方法
/**
* 咖啡饮料是一个抽象类
*
* @author qiudengjiao
*/
public abstract class CaffeineBeverage {
/**
* 现在,用同一个 prepareRecipe() 方法类处理茶和咖啡 prepareRecipe() 被声明为
* final,因为我们不希望子类覆盖这个方法 我们将步骤二和步骤四泛化成为 brew() 和 addCondiments()
*/
final void prepareRecipe() {
builWater();
brew();
pourInCup();
addCondiments();
}
/**
* 煮沸水
*/
private void builWater() {
System.out.println("builWater");
}
/**
* 把饮料倒进杯子
*/
private void pourInCup() {
System.out.println("pourInCup");
}
/**
* 冲泡方法被声明为抽象,具体操作由子类去实现
*/
abstract void brew();
/**
* 加入元素方法被声明为抽象,具体操作由子类去实现
*/
abstract void addCondiments();
}
由于写了详细的注释,故不再做多余介绍
3)最后,我们需要处理咖啡和茶类了,这两个类现在都是依赖超类(咖啡因饮料)来处理冲泡法,所以只需要自行处理冲泡和添加调料部分:
咖啡类:
/**
* 咖啡类
*
* @author qiudengjiao
*/
public class Coffee extends CaffeineBeverage{
@Override
void brew() {
System.out.println("冲泡咖啡");
}
@Override
void addCondiments() {
System.out.println("添加糖和牛奶");
}
}
茶类:
/**
* 茶类
*
* @author qiudengjiao
*/
public class Tea extends CaffeineBeverage {
@Override
void brew() {
System.out.println("冲泡茶");
}
@Override
void addCondiments() {
System.out.println("添加柠檬");
}
}
茶和开咖啡都是继承自咖啡饮料类,只是咖啡喝茶都各自处理了自己不相同的部分,模板方法定义了一个算法的步骤,并允许子类为一个或多个步骤提供实现
接下来我们利用泡茶来追踪一下模板方法是如何工作的,我们会得知在算法内的某些地方,该模板方法控制了算法,让子类能够提供某些步骤的实现...我们开始泡茶:
1)好吧,首先我们需要一个茶对象:
Tea myTea = new Tea();
2)然后我们调用这个模板方法:
myTea.prepareRecipe();
prepareRecipe()方法控制了算法,没有人能够改变它,这个方法也会依赖子类来提供某些或所有步骤的实现。
它会依照算法来制作咖啡因饮料......
3)首先,把水煮沸
boilWater();
这件事情是在咖啡因饮料类(超类)中进行的。
4)接下来,我们需要泡茶,这件事情只有子类才知道要怎么做
brew();
5)现在把茶倒进杯中,所有饮料做法都一样,所以这件事情发生在超类中
pourInCup();
6)最后,我们加进饮料,由于调料是各个饮料独有的,所以由子类来实现它
addCondiments();
总结:由 CaffeineBeverage 类主导一切,它拥有算法,而且保护这个算法,对于子类来说,CaffeineBeverage 类的存在,可以将代码的复用最大化。算法只存在于一个地方,所以容易修改,这个模板方法提供了一个框架,可以让其他的咖啡饮料插件来,新的咖啡因饮料只需要实现自己的方法就可以了,CaffeineBeverage 类专注算法本身,而由子类提供完整的实现。
接下来我们进行代码的总结:
/**
* 这就是我们的抽象类,他被声明为抽象 用来作为基类,其子类必须实现其操作
*
* @author qiudengjiao
*/
public abstract class AbstractClass {
/**
* 这就是模板方法,它被声明为final,以免子类改变这个算法的顺序
*/
final void templateMethod() {
primitiveOperation1();
primitiveOperation2();
concreteOperation();
hook();
}
/**
* 这个方法定义成抽象,由具体子类去实现
*/
abstract void primitiveOperation1();
/**
* 这个方法定义成抽象,由具体子类去实现
*/
abstract void primitiveOperation2();
/**
* 这个具体的方法被定义在抽象类中 将它声明为 final,这样一来子类就无法覆盖它,它可以被模板方法直接调用,或者被子类调用
*/
final void concreteOperation() {
// 这里是实现
}
/**
* 钩子(这是一个具体的方法,但是什么事情都不做)
* 我们也可以有“默认不做事情的方法”,我们称这种方法为钩子,子类可以视情况决定要不要覆盖它们,下面我们会介绍钩子的实际用途
*/
public void hook() {
}
}
1)对模板方法进行挂钩
钩子是一种被声明在抽象类中的方法,但只要有空的或者默认的实现,钩子的存在可以让子类有能力对算法的不同点进行挂钩,要不要挂钩,由子类自行决定,接下来我们来看看钩子的用途。
public abstract class CaffeineBeverageWithHook {
final void prepareRecipe() {
builWater();
brew();
pourInCup();
// 这里我们加了一个小小的条件语句,而该条件是否成立,是由具体的方法customerWantsondiments()决定的
// 如果顾客想要调料,我们才调用addCondiments()
if (customerWantsondiments()) {
addCondiments();
}
}
/**
* 煮沸水
*/
private void builWater() {
System.out.println("builWater");
}
/**
* 把饮料倒进杯子
*/
private void pourInCup() {
System.out.println("pourInCup");
}
/**
* 冲泡方法被声明为抽象,具体操作由子类去实现
*/
abstract void brew();
/**
* 加入元素方法被声明为抽象,具体操作由子类去实现
*/
abstract void addCondiments();
/**
* 我们在这里定义了一个方法,(通常)是空的缺省实现,这个方法只会返回ture,不做别的事情
* 这就是一个钩子,子类可以覆盖这个方法,但不见得一定要这么做
*
* @return
*/
boolean customerWantsondiments() {
return true;
}
}
2)使用钩子
为了使用钩子。我们在子类中覆盖它,在这里钩子控制咖啡因饮料是否执行某部分算法,更明确点就是控制饮料中是否添加调料
带钩子咖啡类:
public class CoffeeWithHook extends CaffeineBeverageWithHook {
@Override
void brew() {
System.out.println("冲泡咖啡");
}
@Override
void addCondiments() {
System.out.println("添加糖和牛奶");
}
public boolean customerWantsondiments() {
String answer = getUserInput();
// 让用户输入他们对调料的决定,根据用户的输入返回 true 或 false
if (answer.toLowerCase().startsWith("y")) {
return true;
} else {
return false;
}
}
/**
* 这段代码询问用户是否想要加糖和奶茶,通过命令行获取用户输入
*
* @return
*/
private String getUserInput() {
String answer = null;
System.out.println("Would you like milk and sugar with your cofee(y/n)?");
BufferedReader bf = new BufferedReader(new InputStreamReader(System.in));
try {
answer = bf.readLine();
} catch (IOException ioe) {
System.err.println("IO error");
}
if (answer == null) {
return "no";
}
return answer;
}
}
带钩子茶类:
public class TeaWithHook extends CaffeineBeverageWithHook {
@Override
void brew() {
System.out.println("冲泡茶");
}
@Override
void addCondiments() {
System.out.println("添加柠檬");
}
/**
* 覆盖了这个钩子,提供了自己的功能
*/
public boolean customerWantsondiments() {
String answer = getUserInput();
// 让用户输入他们对调料的决定,根据用户的输入返回 true 或 false
if (answer.toLowerCase().startsWith("y")) {
return true;
} else {
return false;
}
}
/**
* 这段代码询问用户是否想要加糖和奶茶,通过命令行获取用户输入
*
* @return
*/
private String getUserInput() {
String answer = null;
System.out.println("Would you like milk and sugar with your cofee(y/n)?");
BufferedReader bf = new BufferedReader(new InputStreamReader(System.in));
try {
answer = bf.readLine();
} catch (IOException ioe) {
System.err.println("IO error");
}
if (answer == null) {
return "no";
}
return answer;
}
}
3)执行测试程序
public class BeverageTest {
public static void main(String[] args){
//创建一杯茶
TeaWithHook teaHook = new TeaWithHook();
//创建一杯咖啡
CoffeeWithHook coffeeHook = new CoffeeWithHook();
System.out.println("\nMaking tea...");
teaHook.prepareRecipe();
System.out.println("\nMaking coffee...");
coffeeHook.prepareRecipe();
}
}
4)执行结果
让茶中添加柠檬,咖啡中不添加:
让咖啡中添加糖和牛奶,茶中不添加:
模板方法在Android中有很广泛的应用,Activity和Fragment生命周期方法的回调,AsyncTask异步框架的设计等都使用了模板方法模式,这里我们不展开看代码,大家有兴趣可以自行去阅读
今天就写到这里,如有错误请指出