JavaScript设计模式笔记-模板方法模式

11.1 定义与组成

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

应用一般在于我们有一些平行的子类,各个子类之间有一些相同的行为,也有一些不同的行为,如果相同和不同的行为都混合在各个子类中实现,说明这些相同的行为会在各个子类中出现.这些相同的行为实际上可以被搬移到另外一个单一的地方.

11.2 Coffee or Tea

11.2.1 先泡一杯咖啡

步骤如下

  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
var Coffee=function(){}
Coffee.prototype.boilWater=function(){
console.log("把水煮沸")
}
Coffee.prototype.brewCoffeeGriends=function(){
console.log("用沸水冲泡咖啡")
}
Coffee.prototype.pourInCup=function(){
console.log("把咖啡倒进杯子")
}
Coffee.prototype.addSugarAndMilk=function(){
console.log('加糖和牛奶')
}
Coffee.prototype.init=function(){
this.boilWater()
this.brewCoffeeGriends()
this.pourInCup()
this.addSugarAndMilk
}
var coffee=new Coffee()
coffee.init()

11.2.2 泡一壶茶

  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
var Tea = function(){};
Tea.prototype.boilWater = function(){
console.log( '把水煮沸' );
};
Tea.prototype.steepTeaBag = function(){
console.log( '用沸水浸泡茶叶' );
};
Tea.prototype.pourInCup = function(){
console.log( '把茶水倒进杯子' );
};
Tea.prototype.addLemon = function(){
console.log( '加柠檬' );
};
Tea.prototype.init = function(){
this.boilWater();
this.steepTeaBag();
this.pourInCup();
this.addLemon();
};
var tea = new Tea();
tea.init();

11.2.3 分离出共同点






















泡咖啡 泡茶
把水煮沸 用沸水冲泡咖啡
用沸水冲泡咖啡 用沸水浸泡茶叶
把咖啡倒进杯子 把茶水倒进杯子
加糖和牛奶 加柠檬

我们找到泡咖啡和泡茶主要有以下几点不同

  • 原料不同
  • 泡的方式不同
  • 加入的调料不同

经过抽象之后我们可以整理为以下四步

  1. 把水煮沸
  2. 用沸水冲泡饮料
  3. 把饮料倒入杯子
  4. 加调料

所以不管是冲泡还是浸泡都能给它一个新的方法名称比如说brew().
现在可以用一个抽象父类来表示泡一杯饮料的整个过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var Beverage=function(){};
Beverage.prototype.boilWater=function(){
console.log("把水煮沸")
}
Beverage.prototype.brew=function(){//空方法,由子类重写
}
Beverage.prototype.pourInCup=function(){//空方法,由子类重写
}
Beverage.prototype.addCondiments=function(){//空方法,由子类重写
}
Beverage.prototype.init=function(){
this.boilWater();
this.brew();
this.pourInCup();
this.addCondiments();
}

11.2.4 创建Coffee子类和Tea子类

上面的抽象类方法对我们来说是没有意义的,因为世界上能喝的东西没有一种真正叫饮料的,饮料在这里还是一个抽象类的存在,所以我们要创建茶类和咖啡类.

1
2
var Coffee=function(){}
Coffee.prototype=new Beverage();

接下来重写抽象父类中的一些方法,只有’把水煮沸’这个行为可以直接使用父类的Beverage中的boilWater方法,其他需要在子类中重写.

1
2
3
4
5
6
7
8
9
10
11
12
Coffee.prototype.brew=function(){
console.log("用沸水冲泡咖啡")
}
Coffee.prototype.pourInCup = function(){
console.log( '把咖啡倒进杯子' );
};
Coffee.prototype.addCondiments = function(){
console.log( '加糖和牛奶' );
};
var Coffee = new Coffee();
Coffee.init();

至此我们的Coffee类就已经完成了,当调用init的方法会顺着原型链上去找到父类的init方法.而父类的init方法已经规定好了冲泡饮料的顺序,所以我们能成功的泡出一杯咖啡

1
2
3
4
5
6
Beverage.prototype.init=function(){
this.boilWater();
this.brew();
this.pourInCup();
this.addCondiments();
}

那么tea类也是同理

1
2
3
4
5
6
7
8
9
10
11
12
13
var Tea = function(){};
Tea.prototype = new Beverage();
Tea.prototype.brew = function(){
console.log( '用沸水浸泡茶叶' );
};
Tea.prototype.pourInCup = function(){
console.log( '把茶倒进杯子' );
};
Tea.prototype.addCondiments = function(){
console.log( '加柠檬' );
};
var tea = new Tea();
tea.init();

本章讨论的是模板方法模式.上例子中 Beverage.prototype.init被称为模板方法,原因是 该方法中封装了子类的算法框架,它作为一个算法模板,指导子类以何种顺序去执行哪些方法.

11.3 抽象类

模板方法是一种严重依赖抽象类的设计模式,JavaScript在语言层面并没有提供对抽象类的支持,我们也很难模拟抽象类的实现。

11.3.1 抽象类的作用

在java中 类分为两种 一种是具体类 一种是抽象类.具体类可以被实例化,抽象类不能被实例化.
抽象类可以表示一种契约,继承了这个抽象类的子类必须要重写以上的方法 抽象类只是个框架 具体的填充还得有子类来填充 同时控制权也在抽象类的模板方法中.
就拿上例来说,如果coffee类没有重写抽象类需求的4个函数中的一个 那么饮料是无法泡成功的.

11.3.2 抽象方法和具体方法

抽象方法被声明在抽象类中,抽象方法并没有具体实现过程,当子类继承了这个抽象类时,必须重写父类的抽象方法.
除了抽象方法外,如果每个子类中有一些同样的具体实现方法,那么这些方法也可以选择放在抽象类中.这里可以节省代码已达到复用的效果.这些方法叫做具体方法.比如饮料中的boilWater方法,假设冲泡所有的饮料之前都需要把水煮沸.那我们自然可以把boilWater方法放在抽象类Beverage中

11.3.3 java实现Coffee or Tea 例子

略,毕竟java大法好.

11.3.4 JavaScript没有抽象类的缺点和解决方案.

这段的意思是.如果某个baka程序猿忘记重写父类的某个抽象类的,如果处理?一般来说在JavaScript都会选择抛出一段异常来提醒程序猿 比如.

1
2
3
Beverage.prototype.brew=function(){
throw new Error("子类必须重写brew方法")
}

11.4 模板方法使用的场景

从大的方面讲 模板方法模式常被架构师用于搭建项目的框架,构架师定好了框架的骨架,程序猿继承框架的结构之后,负责往里面填空.
在web开发中也能找到很多模板方法适用的场景 比如构建一系列的UI组件这些组件构建的过程一般如下所示:

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

我们可以看到,任何组件的构建都遵循上面的4步,其中1,4是相同的 2,3可以通过子类重写.

11.5 钩子方法

我们在父类中封装了子类的算法框架,这些算法在正常的状态下是适用的,但是有一些个性的子类,比如在冲泡饮料Beverage这个类中封装了以下算法.

  1. 把水煮沸
  2. 用沸水冲泡饮料
  3. 把饮料倒进杯子
  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
var 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();
}
};
var 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( '请问需要调料吗?' );
};
var coffeeWithHook = new CoffeeWithHook();
coffeeWithHook.init()

11.6 好莱坞原则

实际上就控制权在父类手里 不在子类手里 或者说在发布者手里不在订阅者手里
比如订阅-发布模式 控制权在发布者手里 什么时候触发订阅消息,什么时候订阅者才能收到.
回调函数也是同理

11.7 真的需要继承吗

JavaScript实际上没有提供真正的类式继承.继承是通过对象与对象之间的委托来实现的.也就是我们在形式上借鉴了类似继承的语言,但本章学习到的模板方法并不正宗.而且在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
41
42
43
44
45
46
47
48
49
var Beverage=function(param){
var boilWater=function(){
console.log("把水煮沸")
}
var brew=param.brew||function(){
throw new Error("子类必须重写 brew方法")
}
var pourInCup=param.pourInCup||function(){
throw new Error("子类必须重写 pourInCup方法")
}
var addCondiments=param.addCondiments||function(){
throw new Error("子类必须重写 addCondiments方法")
}
var F=function(){}
F.prototype.init=function(){
boilWater();
brew();
pourInCup();
addCondiments();
}
return F;
}
var Coffee = Beverage({
brew: function(){
console.log( '用沸水冲泡咖啡' );
},
pourInCup: function(){
console.log( '把咖啡倒进杯子' );
},
addCondiments: function(){
console.log( '加糖和牛奶' );
}
});
var Tea = Beverage({
brew: function(){
console.log( '用沸水浸泡茶叶' );
},
pourInCup: function(){
console.log( '把茶倒进杯子' );
},
addCondiments: function(){
console.log( '加柠檬' );
}
});
var coffee = new Coffee();
coffee.init();
var tea = new Tea();
tea.init();

11.8 小结

模板方法模式是一种典型的通过封装变化提高系统扩展性的设计模式.通过增加新的子类,我们便可以给系统增加新的功能,并不需要改动抽象父类以及其他子类,也这是符合开发-封闭原则的.
在JavaScript中 高阶函数是更好的选择