JavaScript设计模式与开发实践(七)模板方法模式

模板方法模式

定义一个抽象类,将部分逻辑(公共逻辑)用具体方法以及具体构造函数的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。

不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现。在父类中 封装了子类的算法框架 ,它作为一个算法的模板, 指导子类以何种顺序去执行哪些方法

使用场景

比如我们在构建一系列的 UI 组件,这些组件的构建过程一般如下所示:

  • 初始化一个 div 容器;
  • 通过 ajax 请求拉取相应的数据;
  • 把数据渲染到 div 容器里面,完成组件的构造;
  • 通知用户组件渲染完毕。

我们看到,任何组件的构建都遵循上面的 4 步,其中第(1)步和第(4)步是相同的。第(2)步不同的地方只是请求 ajax 的远程地址,第(3)步不同的地方是渲染数据的方式。

于是我们可以把这 4 个步骤都抽象到父类的模板方法里面,父类中还可以顺便提供第(1)步和第(4)步的具体实现。当子类继承这个父类之后,会重写模板方法里面的第(2)步和第(3)步。

Java实现

在像Java,C++等静态类型语言中,模板方法模式的实现非常依赖抽象类,通过抽象类定义模板方法(方法的执行顺序),以及一些公共方法的实现。子类通过继承抽象类实现剩余的方法。

抽象类的模板方法也会帮我们检测子类对于剩余逻辑的实现情况(如果子类没有实现,编译器就会报错)。

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
77
public abstract class Beverage { // 饮料抽象类

final void init() { // 模板方法
boilWater();
brew();
pourInCup();
addCondiments();
}

void boilWater() { // 具体方法 boilWater
System.out.println("把水煮沸");
}

abstract void brew(); // 抽象方法 brew

abstract void addCondiments(); // 抽象方法 addCondiments

abstract void pourInCup(); // 抽象方法 pourInCup
}

public class Coffee extends Beverage { // Coffee 类

@Override
void brew() { // 子类中重写 brew 方法
System.out.println("用沸水冲泡咖啡");
}

@Override
void pourInCup() { // 子类中重写 pourInCup 方法
System.out.println("把咖啡倒进杯子");
}

@Override
void addCondiments() { // 子类中重写 addCondiments 方法
System.out.println("加糖和牛奶");
}
}

public class Tea extends Beverage { // Tea 类

@Override
void brew() { // 子类中重写 brew 方法
System.out.println("用沸水浸泡茶叶");
}

@Override
void pourInCup() { // 子类中重写 pourInCup 方法
System.out.println("把茶倒进杯子");
}

@Override
void addCondiments() { // 子类中重写 addCondiments 方法
System.out.println("加柠檬");
}
}

public class Test {

private static void prepareRecipe(Beverage beverage) {
beverage.init();
}

public static void main(String args[]) {
Beverage coffee = new Coffee(); // 创建 coffee 对象
prepareRecipe(coffee); // 开始泡咖啡
// 把水煮沸
// 用沸水冲泡咖啡
// 把咖啡倒进杯子
// 加糖和牛奶
Beverage tea = new Tea(); // 创建 tea 对象
prepareRecipe(tea); // 开始泡茶
// 把水煮沸
// 用沸水浸泡茶叶
// 把茶倒进杯子
// 加柠檬
}
}

Javascript实现

同时如果我们不想加糖,我们可以定义一个公共方法( 称为钩子方法 ),在模板方法中 通过钩子方法控制是否执行某个逻辑

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
const Beverage = function () { };
Beverage.prototype.boilWater = function () {
console.log('把水煮沸');
};
Beverage.prototype.brew = function () {
throw new Error('子类必须重写 brew 方法');
};
Beverage.prototype.pourInCup = function () {
throw new Error('子类必须重写 pourInCup 方法');
};
Beverage.prototype.addCondiments = function () {
throw new Error('子类必须重写 addCondiments 方法');
};
Beverage.prototype.customerWantsCondiments = function () {
return true; // 默认需要调料
};
Beverage.prototype.init = function () {
this.boilWater();
this.brew();
this.pourInCup();
if (this.customerWantsCondiments()) { // 如果挂钩返回 true,则需要调料
this.addCondiments();
}
};
const CoffeeWithHook = function () { };
CoffeeWithHook.prototype = new Beverage();
CoffeeWithHook.prototype.brew = function () {
console.log('用沸水冲泡咖啡');
};
CoffeeWithHook.prototype.pourInCup = function () {
console.log('把咖啡倒进杯子');
};
CoffeeWithHook.prototype.addCondiments = function () {
console.log('加糖和牛奶');
};
CoffeeWithHook.prototype.customerWantsCondiments = function () {
return window.confirm('请问需要调料吗?');
};
const coffeeWithHook = new CoffeeWithHook();
coffeeWithHook.init();

用JavaScript模拟抽象类和子类来并不是一个很好的选择,在Javascript中我们还可以使用高阶函数的形式,将需要在子类中实现的细节通过参数的形式传入。

把 brew、pourInCup、addCondiments 这些方法依次传入 Beverage 函数,Beverage 函数被调用之后返回构造器 F。F 类中包含了“模板方法”F.prototype.init。跟继承得到的效果一样,该“模板方法”里依然封装了饮料子类的算法框架。

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
const Beverage = function (param) {
const boilWater = function () {
console.log('把水煮沸');
};
const brew = param.brew || function () {
throw new Error('必须传递 brew 方法');
};
const pourInCup = param.pourInCup || function () {
throw new Error('必须传递 pourInCup 方法');
};
const addCondiments = param.addCondiments || function () {
throw new Error('必须传递 addCondiments 方法');
};
const F = function () { };
F.prototype.init = function () {
boilWater();
brew();
pourInCup();
addCondiments();
};
return F;
};
const Coffee = Beverage({
brew: function () {
console.log('用沸水冲泡咖啡');
},
pourInCup: function () {
console.log('把咖啡倒进杯子');
},
addCondiments: function () {
console.log('加糖和牛奶');
}
});
const Tea = Beverage({
brew: function () {
console.log('用沸水浸泡茶叶');
},
pourInCup: function () {
console.log('把茶倒进杯子');
},
addCondiments: function () {
console.log('加柠檬');
}
});
const coffee = new Coffee();
coffee.init();
const tea = new Tea();
tea.init();

好莱坞原则

允许底层组件将自己挂钩到高层组件中,而高层组件会决定什么时候、以何种方式去使用这些底层组件。

模板方法模式是好莱坞原则的一个典型使用场景,当我们用模板方法模式编写一个程序时,就意味着子类放弃了对自己的控制权,而是改为父类通知子类,哪些方法应该在什么时候被调用。作为子类,只负责提供一些设计上的细节。


JavaScript设计模式与开发实践(七)模板方法模式
https://jing-jiu.github.io/jing-jiu/2023/01/12/notebooks/JavaScript设计模式与实践/设计模式(七)/
作者
Jing-Jiu
发布于
2023年1月12日
许可协议