模板方法模式

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

实际上模版方法模式很简单,就是单纯的利用了子类可以继承父类的方法的这一特性。

下面以一个咖啡和茶的例子来说明这个问题
假设我们泡一杯咖啡有下面的四个步骤

  • 煮开水
  • 冲泡咖啡
  • 把泡好的咖啡放进倒进杯子里
  • 加糖和牛奶

泡茶也是同样的四个步骤

  • 煮开水
  • 冲泡茶包
  • 把泡好的茶放进倒进杯子里
  • 加柠檬

如果我们要实现上面的功能,正常情况下可能需要实现两个类,一个coffee,一个tea,但是这两个类中有共同的部分,都需要煮开水,都需要倒进杯子里。那么自然的我们就想到要抽象出一个父类,然后coffee/tea基层自父类就好了啊。
因此我们有如下的设计方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public abstract class CaffeineBeverage {
final void prepare() {
boilWater();
brew();
pourInCup();
addCondiments();
}
abstract void brew();
abstract void addCondiments();
void boilWater() {
System.out.println("Boiling water!");
}
void pourInCup() {
System.out.println("Pouring into cup!");
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Coffee extends CaffeineBeverage {
public void brew() {
System.out.println("Dripping Coffee through filter");
}
public void addCondiments() {
System.out.println("Adding Sugar and Milk");
}
}
public class Tea extends CaffeineBeverage {
public void brew() {
System.out.println("Steeping the tea");
}
public void addCondiments() {
System.out.println("Adding Lemon");
}
}
1
2
3
4
5
6
7
8
public class TemplatePatternTest {
public static void main(String[] args) {
Coffee coffee = new Coffee();
coffee.prepare();
Tea tea = new Tea();
tea.prepare();
}
}

下面我们看下我们的抽象类CaffeineBeverage, 它有一个final的prepare方法,这是因为我们不想让子类来覆盖这个方法,这样防止了被篡改。
他定义了两个abstract的方法,这两个方法需要子类自己去实现,这样才能让我们作出coffee和tea两种不同的饮料。

下面的这张图显示了我们应该怎样定义模板方法模式

TemplatePattern

对模板方法进行挂钩子

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

还是上面的例子,假设在我们的饮料中加不加糖由子类自己决定,那么该怎样处理呢?

我们可以新创建一个CoffeeWithoutSugar的类,这个类中addCondiments可以空实现,这样就可以解决,但是这样的方案好么?这样会导致类爆炸。

我们可以用钩子来解决。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public abstract class CaffeineBeverageWithHook {
final void prepare() {
boilWater();
brew();
pourInCup();
if (customerWantsCondiments()) {
addCondiments();
}
}
abstract void brew();
abstract void addCondiments();
void boilWater() {
System.out.println("Boiling water!");
}
void pourInCup() {
System.out.println("Pouring into cup!");
}
boolean customerWantsCondiments() {
return true;
}
}

这个就是钩子。实际上我们在相应的位置出加上判断函数(这个bool函数实际上就是钩子),子类需要实现这个钩子,进而能够影响到父类中的算法实现。

1
2
3
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
public class CoffeeWithHook extends CaffeineBeverageWithHook {
public void brew() {
System.out.println("Dripping Coffee through filter");
}
public void addCondiments() {
System.out.println("Adding Sugar and Milk");
}
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("Would like with sugar and milk?");

BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
try {
answer = in.readLine();
} catch (IOException ioe) {
System.err.println("IO Error trying to read your answer!");
}

if (answer == null) {
return "no";
}
return answer;
}
}

下面引入一个新的设计原则

好莱坞原则:别调用我们,我们会调用你

好莱坞原则的引入是为了防止“依赖腐败”。在某些场景下高层组件会依赖低层组件,低层组件又依赖高层组件,而高层组件又依赖边层组件,边层组件又依赖低层组件,这样组件间就会导致依赖腐败,进而系统变得越来越复杂。

而在好莱坞原则下,允许低层组件挂钩到系统上,高层组件会决定什么时候使用这些低层组件。实际上高层组件对低层组件的方式是“别调用我们,我们会调用你”

我们的CoffeineBeverage就是高层组件,它控制了冲泡的算法,只有在需要子类实现的算法时,才会调用子类。

代码归档路径

https://github.com/changyuanchn/DesignPattern/tree/main/src/com/changyuan/TemplatePattern

显示 Gitment 评论