设计模式之模板方法模式

设计模式之模板方法模式

封装算法,好让子类可以在任何时候都可以将自己挂接近运算里。

咖啡示例

咖啡师傅训练手册:各位师傅!准备饮料时,请精确地遵循下面的冲泡法

咖啡冲泡法:

  1. 把水煮沸
  2. 用沸水冲泡咖啡
  3. 把咖啡倒进杯子
  4. 加糖和牛奶

茶冲泡法:

  1. 把水煮沸
  2. 用沸水冲泡茶叶
  3. 把茶倒进杯子
  4. 加柠檬

快速搞定咖啡和茶的类

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
/**
* 咖啡
* @author Mr.zxb
* @date 2020-08-25 21:38:09
*/
public class Coffee {
/**
* 制作咖啡
*/
void prepareRecipe() {
// 烧水
boilWater();
// 冲泡咖啡粉
brewCoffeeGrinds();
// 倒入杯子
pourInCup();
// 加糖和牛奶
addSugarAndMilk();
}

private void addSugarAndMilk() {
System.out.println("Adding Sugar and Milk");
}

private void pourInCup() {
System.out.println("Pouring into cup");
}

private void brewCoffeeGrinds() {
System.out.println("Dripping Coffee through filter");
}

private void boilWater() {
System.out.println("Boiling water");
}
}
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
/**
* 茶
* @author Mr.zxb
* @date 2020-08-25 21:41:18
*/
public class Tea {
/**
* 制作茶
*/
void prepareRecipe() {
// 烧水
boilWater();
// 冲泡茶包
steepTeaBag();
// 倒入杯子
pourInCup();
// 加柠檬
addLemon();
}

private void addLemon() {
System.out.println("Adding Lemon");
}

private void pourInCup() {
System.out.println("Pouring into cup");
}

private void steepTeaBag() {
System.out.println("Steeping the tea");
}

private void boilWater() {
System.out.println("Boiling water");
}
}

抽象 prepareRecipe()

将咖啡和茶的制作共同点抽取出来,让我们每个子类中逐步抽象prepareRecipe()…

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
/**
* 咖啡因饮料抽象类
* @author Mr.zxb
* @date 2020-08-25 21:52:53
*/
public abstract class CaffeineBeverage {
/**
* 我们不希望子类修改这个方法
*/
final void prepareRecipe() {
boilWater();
brew();
pourInCup();
addCondiments();
}

/**
* 冲泡,因为咖啡和茶的冲泡方法不一样,交给子类去实现
*/
abstract void brew();

/**
* 添加调味品,子类可以自定义添加调味品
*/
abstract void addCondiments();

/**
* 装杯
*/
private void pourInCup() {
System.out.println("Pouring into cup");
}

/**
* 烧水
*/
private void boilWater() {
System.out.println("Boiling water");
}
}

改造我们的咖啡和茶类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 咖啡
* @author Mr.zxb
* @date 2020-08-25 21:38:09
*/
public class Coffee extends CaffeineBeverage {

@Override
protected void brew() {
System.out.println("Dripping Coffee through filter");
}

@Override
protected void addCondiments() {
System.out.println("Adding Sugar and Milk");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 茶
* @author Mr.zxb
* @date 2020-08-25 21:41:18
*/
public class Tea extends CaffeineBeverage {

@Override
protected void brew() {
System.out.println("Steeping the tea");
}

@Override
protected void addCondiments() {
System.out.println("Adding Lemon");
}
}

认识模板方法

基本上,我们刚刚实现的就是模板方法模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public abstract class CaffeineBeverage {
// prepareRecipe() 就是我们的模板方法,该模板方法里的有些方法是超类处理,有些是子类处理
final void prepareRecipe()() {
boilWater();
brew();
pourInCup();
addCondiments();
}
// 需要由子类提供的方法,必须在超类中声明为抽象
protected abstract void brew();

protected abstract void addCondiments();

private void pourInCup() {
// 实现
}

private void boilWater() {
// 实现
}
}

模板方法定义了一个算法的步骤,并允许子类为一个或多个步骤提供实现。

走,泡茶去…..

  1. 首先我们需要一个茶对象…

    1
    Tea myTea = new Tea();
  2. 然后我们调用这个模板方法

    1
    myTea.prepareRecipe();
  3. 首先,把水煮沸,由超类进行

    1
    boilWater();
  4. 接下来,我们需要泡茶,这件事情只有子类才知道怎么做

    1
    brew();
  5. 现在把茶倒进杯子中,所有的饮料做法都一样,所以这件事情发生在超类中

    1
    pourInCup();
  6. 最后,我们加紧调料,由于调料是各个饮料独有的,所以由子类来实现它

    1
    addCondiments();

定义模板方法模式

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

对模板方法进行挂钩

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

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
/**
* 咖啡因饮料抽象类
* @author Mr.zxb
* @date 2020-08-25 21:52:53
*/
public abstract class CaffeineBeverageWithHook {
/**
* 我们不希望子类修改这个方法
*/
final void prepareRecipe() {
boilWater();
brew();
pourInCup();
// 如果顾客想要调料,才调用添加调料的方法
if (customerWantsCondiments()) {
addCondiments();
}
}

/**
* 冲泡,因为咖啡和茶的冲泡方法不一样,交给子类去实现
*/
protected abstract void brew();

/**
* 添加调味品,子类可以自定义添加调味品
*/
protected abstract void addCondiments();

/**
* 装杯
*/
private void pourInCup() {
System.out.println("Pouring into cup");
}

/**
* 烧水
*/
private void boilWater() {
System.out.println("Boiling water");
}

/**
* 这是一个钩子,子类可以覆盖这个方法, 但不见得要这么做
* @return
*/
boolean customerWantsCondiments() {
return true;
}
}

使用钩子

为了使用钩子,我们在子类中覆盖它。在这里,钩子控制了咖啡因饮料是否执行某部分算法;说得更明确一些,就是饮料中是否要加进调料。

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
/**
* 茶
* @author Mr.zxb
* @date 2020-08-25 21:41:18
*/
public class TeaWithHook extends CaffeineBeverageWithHook {

@Override
protected void brew() {
System.out.println("Steeping the tea");
}

@Override
protected void addCondiments() {
System.out.println("Adding Lemon");
}

/**
* 覆盖钩子,实现自己的功能,根据用户输入,是否需要添加配料
* @return
*/
@Override
boolean customerWantsCondiments() {
return getUserInput().toLowerCase().startsWith("y");
}

/**
* 获取用户是否要加调料
* @return
*/
private String getUserInput() {
System.out.print("Would you like lemon with your tea (y/n)?");

BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
try {
return in.readLine();
} catch (IOException e) {
e.printStackTrace();
}
return "no";
}
}

执行测试程序

1
2
3
4
5
6
7
8
public class BeverageTestDrive {
public static void main(String[] args) {
TeaWithHook teaWithHook = new TeaWithHook();

System.out.println("Make tea...");
teaWithHook.prepareRecipe();
}
}
1
2
3
4
5
6
Make tea...
Boiling water
Steeping the tea
Pouring into cup
Would you like lemon with your tea (y/n)? y
Adding Lemon

好莱坞原则

我们有一个新的设计原则,称为好莱坞原则:

好莱坞原则:别调用(打电话给)我们,我们会调用(打电话给)你。

好莱坞原则和模板方法

好莱坞原则和模板方法之间的连接还算明细:当我们设计模板方法模式时,我们告诉子类,“不要调用我们,我们会调用你”。

总结

  • 模板方法定义了算法的步骤,把这些步骤的实现延迟到了子类
  • 模板方法模式为我们提供了一种代码复用的重要技巧
  • 模板方法的抽象类可以定义具体方法、抽象方法和钩子
  • 抽象方法由子类实现
  • 钩子是一种方法,它在抽象类中不做事,或者只做默认的事情,子类可以选择要不要去覆盖它
  • 为了防止子类改变模板方法中的算法,可以将模板方法声明为final
  • 好莱坞原则告诉我们,将决策权放在高层模块中,以便决定如何以及何时调用底层模块
  • 你将在真实世界代码中看到模板方法模式的许多变体,不要期待它们全都是一眼就可以被你认出的
  • 策略模式和模板方法模式都封装算法,一个用组合,一个用继承
  • 工厂方法是模板方法的一种特殊版本