JavaScript设计模式之模板方法模式

模板方法是我们在编码中最常接触的一种设计模式,只需使用继承就可以实现的非常简单的模式。

定义

模板方法模式由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类。通常 在抽象父类中封装了子类的算法框架,包括实现一些公共方法以及封装子类中所有方法的执行顺 序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。其UML图如下所示

  • Abstract Class 在抽象模板中,定义算法框架,主要步骤等
  • Concrete Class 在具体模板中来实现父类中的具体步骤

实现

下面我们以茶与咖啡这个经典例子来向大家清楚的介绍什么是模板方法。想象一下,我们在泡茶与咖啡时通常有以下的共同点:

  • 将水煮沸
  • 用沸水冲泡茶或咖啡
  • 将冲泡好的茶或咖啡倒入杯中
  • 加入辅料,例如茶可以加些柠檬、咖啡可以加些牛奶和糖

经过以上分析,我们发现咖啡和茶的冲泡过程是大同小异的,因此我们可以将这些相似的步骤抽象出来一个饮料来当做模板方法中的抽象模板,而茶和咖啡作为具体模板继承饮料抽象模板实现其相应的方法就可以了。

问题一

在 Java 中编译器会保证子类会重写父类中的抽象方法,但在 JavaScript 中却没有进行这些检查工作。我们在编写代码的时候得不到任何形式的警告,完全寄托于程序员的记忆力和自觉性是很危险的,特别是当我们使用模板方法模式这种完全依赖继承而实现的设计模式时。

问题二

虽然以上4个步骤适用于绝大多数的人,但是存在一些人不喜欢在泡茶或咖啡的时候加入辅料。对于已经规定好了冲泡饮料的 4 个步骤,那么有什么办法可以让子类不受这个约束呢?

解决方案

针对于问题一,我们可以在抽象模板中需要子类重写的方法中抛出一个错误,如果子类忘记重写该方法,则程序在运行时会报错。

针对于问题二,钩子方法(hook)可以用来解决这个问题,放置钩子是隔离变化的一种常见手段。我们在父类中容易变化的地方放置钩子,钩子可以有一个默认的实现,究竟要不要“挂钩”,这由子类自行决定。

下面我用代码来模拟这个例子,首先我们来实现抽象模板——饮料类

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
// 抽象模板--饮料类
class Beverage {

init() {
this.boilWater()
this.brew()
this.pourInCup()
if (this.customerWantsCondiments()) { // 如果挂钩返回true,则需要调料
this.addCondiments()
}
}

/**
* 烧水
*/
boilWater() {
console.log('把水煮沸')
}

/**
* 用沸水冲泡
*/
brew() {
throw new Error('子类必须重写brew方法')
}

/**
* 倒进杯中
*/
pourInCup() {
throw new Error('子类必须重写pourInCup方法')
}

/**
* 添加辅料
*/
addCondiments() {
throw new Error('子类必须重写addCondiments方法')
}

/**
* 是否需要添加辅料钩子函数
*/
customerWantsCondiments() {
return true //默认为true 需要
}
}

茶类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Tea extends Beverage{
brew() {
console.log('用沸水冲泡茶')
}

pourInCup() {
console.log('将茶倒入杯中')
}

addCondiments() {
console.log('添加柠檬')
}

customerWantsCondiments() {
return false // 不要辅料
}
}

咖啡类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Coffee extends Beverage{
brew() {
console.log('用沸水冲咖啡')
}

pourInCup() {
console.log('将咖啡倒入杯中')
}

addCondiments() {
console.log('添加糖和牛奶')
}

customerWantsCondiments() {
return true // 添加辅料
}
}

const coffee = new Coffee()
coffee.init()

使用场景

假如我们有一些平行的子类,各个子类之间有一些相同的行为,也有一些不同的行为。如果相同和不同的行为都混合在各个子类的实现中,说明这些相同的行为会在各个子类中重复出现。但实际上,相同的行为可以被搬移到另一个单一的地方,模板方法模式就是为解决这个问题而生的。在模板方法模式中,子类实现中的相同部分被上移到父类中,而将不同的部分留待子类来实现。