模板方法模式是一种只需使用继承就可以实现的模式。
模板方法模式由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类。
通常在抽象父类中封装了子类的算法框架,包括实现一些公共方法以及封装子类中所有方法的执行顺序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。
接下来通过咖啡与茶这个经典的例子来说明模版方法模式。
首先,我们想泡一杯咖啡,泡咖啡的步骤通常如下:
将上述步骤写成代码描述出来,具体如下:
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();
接下来,我们泡一壶茶,泡茶的步骤和泡咖啡的步骤基本相同:
同样用一段代码来实现泡茶的步骤:
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();
经过对比泡咖啡和泡茶的过程,我们发现两者的步骤好像差不多,但是有如下不同点:
经过抽象之后,不管是泡咖啡还是泡茶,我们都能整理为下面四步:
根据上述共同点,我们可以创建一个抽象父类来表示泡一杯饮料的整个过程。不论是Coffee
,还是Tea
,都用Beverage
来表示,代码如下:
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();
};
接下来我们创建咖啡类和茶类,并让它们继承饮料类:
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("加糖和牛奶");
};
var Coffee = new Coffee();
Coffee.init();
var Tea = function () {};
Tea.prototype = new Beverage();
// 重写父类方法
Tea.prototype.steepTeaBag = function () {
console.log("用沸水浸泡茶叶");
};
Tea.prototype.pourInCup = function () {
console.log("把茶水倒进杯子");
};
Tea.prototype.addCondiments = function () {
console.log("加柠檬");
};
var tea = new Tea();
tea.init();
在上面的例子中,Beverage.prototype.init
就是模板方法,因为该方法中封装了子类的算法框架,它作为一个算法的模板,指导子类以何种顺序去执行哪些方法。
通过模板方法模式,我们在父类中封装了子类的算法框架。这些算法框架在正常状态下是适用于大多数子类的,但如果有一些特别“个性”的子类呢?比如有一些客人喝咖啡是不加调料(糖和牛奶)的。
既然Beverage
作为父类,已经规定好了冲泡饮料的4个步骤,那么有什么办法可以让子类不受这个约束呢?
钩子方法可以用来解决这个问题,放置钩子是隔离变化的一种常见手段。我们在父类中容易变化的地方放置钩子,钩子可以有一个默认的实现,究竟要不要“挂钩”,这由子类自行决定。钩子方法的返回结果决定了模板方法后面部分的执行步骤,也就是程序接下来的走向,这样一来,程序就拥有了变化的可能。
接下来将挂钩customerWantsCondiments
放入Beverage
类,看看我们如何得到一杯不需要糖和牛奶的咖啡,代码如下:
var Beverage = function () {};
Beverage.prototype.boilWater = function () {
console.log("把水煮沸");
};
Beverage.prototype.brew = function () {}; // 冲泡动作
Beverage.prototype.pourInCup = function () {}; // 倒入杯子
Beverage.prototype.addCondiments = function () {}; // 添加调料
Beverage.prototype.customerWantsCondiments = function () {
return true; // 默认需要调料
};
Beverage.prototype.init = function () {
this.boilWater();
this.brew();
this.pourInCup();
if (this.customerWantsCondiments()) { // 如果返回true,表示需要调料
this.addCondiments();
}
};
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("加糖和牛奶");
};
Coffee.prototype.customerWantsCondiments = function () {
return window.confirm("请问需要调料吗?");
};
var Coffee = new Coffee();
Coffee.init();
JS好莱坞原则的基本思想是,高层组件通过调用底层组件暴露的接口来控制底层组件的行为,而底层组件不应该直接依赖或调用高层组件的实现。
这个原则的名字来源于好莱坞电影制片厂,因为在好莱坞电影中,明星演员通常是高层组件,而服装、化妆等其他组件则是底层组件。这些底层组件在电影制作过程中由高层组件来控制和协调。
在这一原则的指导下,我们允许底层组件将自己挂钩到高层组件中,而高层组件会决定什么时候、以何种方式去使用这些底层组件。
模板方法模式是好莱坞原则的一个典型使用场景,它与好莱坞原则的联系非常明显,当我们用模板方法模式编写一个程序时,就意味着子类放弃了对自己的控制权,而是改为父类通知子类,哪些方法应该在什么时候被调用。作为子类,只负责提供一些设计上的细节。
在好莱坞原则的指导之下,下面这段代码可以达到和继承一样的效果。
var Beverage = function (param) {
var boilWater = function () {
console.log("把水煮沸");
};
var brew = param.brew; // 冲泡动作
var pourInCup = param.pourInCup; // 倒入杯子
var addCondiments = param.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();
在这段代码中,我们把brew
、pourInCup
、addCondiments
这些方法依次传入Beverage
函数,Beverage
函数被调用之后返回构造器F
。F
类中包含了“模板方法”F.prototype.init
。跟继承得到的效果一样,该“模板方法”里依然封装了饮料子类的算法框架。