好久没看设计模式了,今天翻开书看了下模板方法模式,看完之后记录下学习笔记。
模板方法模式在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
直接看定义,感觉似懂非懂,很难理解其中的要义。模版方法实际上就是将一类算法(此处的算法可以理解为一类业务)的具体步骤抽象出来,将其放到超类中,不变的步骤直接在超类中实现,变化的步骤让子类实现。这样下来有几个好处,第一是子类中不存在重复代码了,第二是算法的步骤已经在超类中定义好,子类无法修改,只能提供某些步骤的具体实现,第三就是设计更有弹性,降低了耦合。
书上的例子
结合书上的具体例子来理解:泡咖啡和泡茶
泡咖啡 | 泡茶 |
1.把水煮沸 | 1.把水煮沸 |
2.用热水冲泡咖啡 | 2.用热水冲泡茶叶 |
3.把咖啡倒进杯子 | 3.把茶倒进杯子 |
4.加糖和牛奶 | 4.加柠檬 |
可以看出,泡咖啡和泡茶很相似,它们的过程可以理解为一类算法,1 和 3 是一样的操作,2 和 4 只有细微的差异。如果要用代码实现这两个过程,在使用模版方法之前,可以设想几种方式
-
直接写两个类,泡茶类和泡咖啡类,互不相干,各自实现步骤
缺点很明显,重复代码太多,一旦步骤变化,每个类都需要修改
-
将 1 和 3 步骤定义到超类中,然后定义两个类继承它,子类实现 3 和 4 步骤
重复的代码抽象到了超类中,避免了重复代码,常规操作,感觉还行,但是还可以提升
-
将 1 和 3 步骤定义到超类中,将 2 和 4 在超类中实现过程,通过传参数来控制具体逻辑,然后定义两个类继承它,子类传参控制 3 和 4 步骤
看起来好像代码复用更多了,子类代码很少,但是有个致命缺点,子类数量一多,那么超类中 2 和 4 将有大量条件分支,混乱不利于维护
初步的设想都不是很好,这个时候就轮到模版方法来解决上述问题了。
- 定义一个超类,将 1 和 3 在超类中实现,2 和 4 定义为抽象方法,强制子类实现,在超类中定义一个控制方法,按顺序调用步骤1234。
具体实现
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
/**
* 算法的超类
*/
public abstract class CaffeineBeverage {
/**
* 该方法定义了算法的步骤和顺序
*/
final void prepareRecipe() {
boilWater();
brew();
pourInCup();
addCondiments();
}
/**
* 步骤2 定义为抽象方法,又子类实现
*/
abstract void brew();
/**
* 步骤4 定义为抽象方法,又子类实现
*/
abstract void addCondiments();
void boilWater() {
// 1.把水烧开
}
void pourInCup() {
// 2.把茶或咖啡倒入杯子
}
}
/**
* 泡茶的类,继承CaffeineBeverage,实现2和4
*/
public class Tea entends CaffeineBeverage {
@Override
public void brew() {
// 2.用热水冲泡茶叶
}
@Override
public void addCondiments() {
// 4.加柠檬
}
}
/**
* 泡咖啡的类,继承CaffeineBeverage,实现2和4
*/
public class Coffee entends CaffeineBeverage {
@Override
public void brew() {
// 2.用热水冲泡咖啡
}
@Override
public void addCondiments() {
// 4.加糖和牛奶
}
}
// 测试
public class Test {
public void static main(string[] args) {
Tea tea = new Tea();
tea.prepareRecipe();
}
}
以上就是模版方法的具体实现了,超类定义好了算法的步骤和顺序,实现了通用步骤,而子类只需要实现抽象部分就好了。
有几个细节值得注意下:
- 超类是抽象的,不允许直接实例化使用,因为超类本身功能是不全的,必须有子类实现缺失部分
- 由子类实现的部分在超类中是抽象方法
- 定义算法的步骤和顺序的方法是 final 的,不允许子类改变
钩子方法
钩子是一种被声明在抽象类中的方法,但只有空的或者默认的实现。钩子的存在,可以让子类有能力对算法的不同点就行控制,要不要控制,由子类决定。
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
40
41
42
43
44
45
46
47
48
49
50
51
/**
* 算法的超类
*/
public abstract class CaffeineBeverage {
/**
* 该方法定义了算法的步骤和顺序
*/
final void prepareRecipe() {
boilWater();
brew();
pourInCup();
// 利用钩子控制要不要加料,默认返回true
if (hook1()){
addCondiments();
}
// 利用钩子添加其他行为
hook2();
}
/**
* 步骤2 定义为抽象方法,又子类实现
*/
abstract void brew();
/**
* 步骤4 定义为抽象方法,又子类实现
*/
abstract void addCondiments();
void boilWater() {
// 1.把水烧开
}
void pourInCup() {
// 2.把茶或咖啡倒入杯子
}
/**
* 钩子1,默认返回 true,子类可以覆盖
*/
boolean hook1() {
return true;
}
/**
* 钩子2,空方法,子类可以覆盖
*/
void hook2() {
}
}
当创建一个模版方法时,怎么判断是使用钩子还是抽象步骤方法呢?当算法中的这个步骤是可选的时候,使用钩子,如果这个步骤必须由子类实现,使用抽象方法。具体可以结合实际应用选择。
模版方法在平时工作中很常见,如果运用的好的话,可以提升工作效率,设计出逻辑条理更清晰的代码结构,但是并不是所有的应用都跟书上的例子一样,实际情况往往会出现很多变种,需要理解模版方法的精神去进行辨别。