模板方法

什么是模板方法模式

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

一个小例子

我们要泡茶和冲咖啡,而泡茶和冲咖啡有些相同的地方,也有些不同的地方,比如烧水和把饮料(咖啡或者茶)装进杯子这些步骤都是一样的,不同的是咖啡可能和茶制成后添加的调味剂不一样
这样的话我们可以写一个基类把所有相同的步骤抽取出来,而不同的步骤延伸到子类里去进行
首先是含有咖啡因饮料的基类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 含有咖啡因饮料基类
*/
public abstract class CaffeineBeverage {
final void prepareRecipe(){
boilWater();//煮水
brew();//冲泡
pourInCup();//装进杯子
if(customerWantsCondiments()){
addCondiments();//加料
}
}
abstract void brew();
abstract void addCondiments();
void boilWater(){
System.out.println("把水煮沸");
}
void pourInCup(){
System.out.println("倒进杯子");
}
//钩子
boolean customerWantsCondiments(){
return true;
}
}

咖啡

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 咖啡
*/
public class Coffee extends CaffeineBeverage {
@Override
void brew() {
System.out.println("冲泡咖啡");
}
@Override
void addCondiments() {
System.out.println("加点牛奶和糖");
}
}


1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 茶
*/
public class Tea extends CaffeineBeverage {
@Override
void brew() {
System.out.println("冲泡茶叶");
}
@Override
void addCondiments() {
System.out.println("加点柠檬");
}
}

测试类

1
2
3
4
5
6
7
8
9
public class Test {
public static void main(String[] args){
CaffeineBeverage tea = new Tea();
tea.prepareRecipe();
CaffeineBeverage coffee = new Coffee();
coffee.prepareRecipe();
}
}

输出结果
把水煮沸
冲泡茶叶
倒进杯子
加点柠檬
把水煮沸
冲泡咖啡
倒进杯子
加点牛奶和糖

我们把相同的步骤都放到基类里去执行了,而子类只处理关于自己的步骤就好了,这就是一种简单的模板方法的实现
但是有的客户可能有些特殊的要求,比如有的不喜欢在咖啡或者茶里添加一些调料,他们喜欢喝原汁原味的,这个时候钩子就产生了用途

什么是钩子?

钩子是一种被声明在抽象类中的方法,但是只有空的或者默认的实现。钩子的存在可以让子类有能力对算法的不同点进行挂钩,要不要挂钩由子类自己决定

上边的咖啡因基类CaffeineBeverage已经实现了默认的钩子,现在我们让子类来决定要不要使用钩子
首先是咖啡类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* 咖啡钩子类
*/
public class CoffeeWithHook extends CaffeineBeverage {
@Override
void brew() {
System.out.println("coffee");
}
@Override
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 bufferedReader = new BufferedReader(new InputStreamReader(System.in));
try {
answer = bufferedReader.readLine();
} catch (IOException e) {
System.out.println("IOException");
}
if(answer == null){
return "no";
}
return answer;
}
}

茶类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* 茶的钩子类
*/
public class TeaWithHook extends CaffeineBeverage {
@Override
void brew() {
System.out.println("Tea");
}
@Override
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 bufferedReader = new BufferedReader(new InputStreamReader(System.in));
try {
answer = bufferedReader.readLine();
} catch (IOException e) {
System.out.println("IOException");
}
if(answer == null){
return "no";
}
return answer;
}
}

测试类中的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test {
public static void main(String[] args){
/*CaffeineBeverage tea = new Tea();
tea.prepareRecipe();
CaffeineBeverage coffee = new Coffee();
coffee.prepareRecipe();*/
CaffeineBeverage coffeeWithHook = new CoffeeWithHook();
CaffeineBeverage teaWithHook = new TeaWithHook();
System.out.println("\nmaking tea");
teaWithHook.prepareRecipe();
System.out.println("\nmaking coffee");
coffeeWithHook.prepareRecipe();
}
}

输出结果

making tea
把水煮沸
Tea
倒进杯子
是否想要加点柠檬呢(y/n)?
y
加点柠檬

making coffee
把水煮沸
coffee
倒进杯子
是否想要加点牛奶和咖啡呢(y/n)?
n
加了钩子之后我们看到子类可以决定到底是否要调用addCondiments方法来为饮料加点料

一个鸭子排序的模板方法

我们有一个鸭子类实现了Comparable接口,并且重写了compareTo方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* 鸭子类
*/
public class Duck implements Comparable {
String name;
int weight;
public Duck(String name,int weight){
this.name = name;
this.weight = weight;
}
public String toString(){
return name+" weight"+weight;
}
@Override
public int compareTo(Object o) {
Duck otherDuck = (Duck) o;
if(this.weight < otherDuck.weight){
return -1;
}else if(this.weight == otherDuck.weight){
return 0;
}else{//(this.weight > otherDuck.weight)
return 1;
}
}
}

接下来我们要对一堆鸭子根据体重排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Test {
public static void main(String[] args){
Duck[] ducks = {
new Duck("daffy",8),
new Duck("Dewey",2),
new Duck("Howard",7),
new Duck("Louie",2),
new Duck("Donald",10),
new Duck("Huey",2)
};
System.out.println("\n开始排序鸭子之前");
display(ducks);
Arrays.sort(ducks);
System.out.println("\n排序鸭子后");
display(ducks);
}
public static void display(Duck[] ducks){
for(int i = 0,size = ducks.length; i < size; i++){
System.out.println(ducks[i]);
}
}
}

我们使用了Arrays.sort方法实现,输出结果如下

开始排序鸭子之前
daffy weight8
Dewey weight2
Howard weight7
Louie weight2
Donald weight10
Huey weight2

排序鸭子后
Dewey weight2
Louie weight2
Huey weight2
Howard weight7
daffy weight8
Donald weight10

我们观察鸭子排序内部的工作
1.首先我们需要一个鸭子数组:
Duckp[] ducks = {new Duck(“daffy”,8),…..}
2.然后调用Arrays类的sort()方法,并传入鸭子数组:
Arrays.sort(ducks);
for(int i = low; i < high; i++){
…compareTo()…
…swap()…
}
sort()方法控制算法,没有类可以改变这一点。sort()依赖一个Comparable类提供的compareTo()的实现。
3.想要排序一个数组,需要一次次的比较两个项目,直到整个数组都排序完毕
ducks[0].compareTo(ducks[1]);
4.如果鸭子的次序不对,就用Arrays的swap()方法将两者对调
5.排序方法会持续的比较并对调鸭子,直到整个数组的次序是正确的。

总结

1.”模板方法”定义了算法的步骤,把这些步骤的实现延迟到子类
2.模板方法为我们提供了一种代码复用的技巧
3.模板方法的抽象类可以定义具体方法,抽象方法和钩子
4.抽象方法由子类实现
5.钩子是一种方法,它在抽象类中不做事,或者只做默认的事,子类可以选择要不要去覆盖它
6.为了防止子类改变模板方法中的算法,可以将模板方法声明为final
7.“好莱坞”原则告诉我们,将决策权放在放在高层模块中,以便决定如何以及何时调用底层模块
8.真实的模板方法使用犹如上边的鸭子排序一样,很难一眼被我们认识
9.策略模式和模板方法模式都封装算法,一个用组合,一个用继承
10.工厂方法是模板方法的一种特殊版本