1 模板方法模式定义
模板方法模式:只需要使用集成就能实现。由两部分组成:抽象父类 + 具体的实现子类。
- 抽象父类:封装子类的算法框架,包括实现一些公用方法以及封装在子类中所有方法的执行顺序
- 实现子类:通过集成这个抽象类,也继承了整个算法,并且可以选择重写父类的方法 假如我们有许多平行的类,各个类之间有许多相同的行为,也有部分不同的行为。如果各位都定义自己所有的行为,那么会出现很多重复的方法。此时可以将相同的行为搬移到另外一个单一的地方,模板方法模式就是为了解决这个问题。在模板方法模式中,子类中相同的行为被移动到了父类中,而将不同的部分留待子类来实现。
2 咖啡与茶(模板方法模式案例)
我们现在需要泡一杯茶和咖啡,思考泡茶与咖啡的过程。

不管泡茶还是咖啡都会有四个步骤,总结出来如下。我们抽象一个父类表示泡一杯饮料的过程。
- 煮沸水,相同点
- 沸水+原料(不同点,咖啡,茶叶)
- 将饮料倒入杯子,相同点
- 加调料(不同点:糖与牛奶,柠檬)
抽象父类
由于javascript没有类型检查,我们需要让子类必须实现brew, pourInup和addCondiments,因此这里通过抛出异常来提醒编写者。
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.init = function() {
this.boilWater();
this.brew();
this.pourInCup();
this.addCondiments();
}
创建Coffee子类
接下来要重写抽象父类中的一些方法。只要把水煮沸这个行为可以直接使用父类。
var Coffee = function() { };
Coffee.prototype = new Beverage();
Coffee.prototype.brew = function(){
console.log( '用沸水冲泡咖啡' );
};
Coffee.prototype.pourInCup = function(){
console.log( '把咖啡倒进杯子' );
};
Coffee.prototype.addCondiments = function(){
console.log( '加糖和牛奶' );
};
// 当调用init方法时,会找到父类的init方法进行调用。
var coffee = new Coffee();
coffee.init();
创建Tea子类
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();
模板方法
上面的例子,tea和Coffee都继承了Beverage,那么谁是模板方法呢?Beverage.prototype.init就是模板方法。因为它内部封装了子类的算法框架,它作为一个算法的模板,知道子类以何种顺序执行哪些方法。
3 钩子方法
平时遇到正常的,喝咖啡的都是上面的顺序,但是如果有些人不喜欢加调料,那么上面的步骤又不符合情况了,此时可以通过钩子函数来进行解决。钩子函数通过用户返回的结果来决定接下来的步骤。究竟要不要钩子由子类自己决定。
var Beverage = function() { };
Beverage.prototype.boilWater = function() {
console.log('煮沸水');
};
Beverage.prototype.brew = function(){
throw new Error( '子类必须重写 brew 方法' );
};
....
// 钩子函数
Beverage.prototype.customerWantsCondiments = function(){
return true;
};
Beverage.prototype.init = function() {
this.boilWater();
this.brew();
this.pourInCup();
// 如果钩子函数返回true,则添加调料
if (this,customerWantsCondiments()) {
this.addCondiments();
}
}
在子类Coffee中,需要实现钩子函数
var Coffee = function() { };
Coffee.prototype = new Beverage();
Coffee.prototype.brew = function(){
console.log( '用沸水冲泡咖啡' );
};
...
// 重写钩子函数
Coffee.prototype.customerWantsCondiments = function(){
return window.confirm('请问需要调料吗?');
};
// 当调用init方法时,会找到父类的init方法进行调用。
var coffee = new Coffee();
coffee.init();
4 真的需要继承吗
模板方法模式就是基于继承的一种设计模式,父类中封装了子类的算法框架和执行顺序,子类继承父类后,父类通知子类执行这些方法。但是javascript并没有提供真正的类式继承,继承是通过对象与对象之间的委托来实现的,也就是形式上借鉴了提供类式的语言。下面这段代码能够达到一样的继承效果。
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( '加糖和牛奶' );
}
});
5 小结
模板方法模式在传统的编程语言中,子类的方法种类以及执行顺序都是不变的,这部分逻辑我们都抽象到了父类中,而子类的方法具体怎么实现是可变的,通过重写父类的方法,将变化的逻辑部分封装到子类中。通过增加新的子类,我们能够给系统增加新的功能的,俺是并不需要修改父类以及其他的子类,这也符合开放-封闭原则。在javascript中,我们不需要依样画瓢去实现一个模板方法模式,因为高阶函数是一个更好的选择。