模板方法模式是一种只需要使用继承就可以实现的非常简单的模式。
模板方法模式由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类。通常在抽象父类中封装了子类的算法框架,包括实现一些公共方法以及封装子类中所有方法的执行顺序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。
第一个例子-- Coffee or Tea
1.先泡一杯咖啡
(1)把水煮沸
(2)用沸水冲泡咖啡
(3)把咖啡倒进杯子
(4)加糖和牛奶
通过下面这一段代码,我们就能得到一杯香浓的咖啡
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();
2.泡一壶茶
(1)把水煮沸
(2)用沸水浸泡茶叶
(3)把茶水倒进杯子
(4)加柠檬
var Tea = function(){};
Tea.prototype.boilWater = function(){
console.log('把水煮沸');
}
Tea.prototype.brewTeaBag = function(){
console.log('用沸水浸泡茶叶');
}
Tea.prototype.pourInCup = function(){
console.log('把茶水倒进杯子');
}
Tea.prototype.addlemon = function(){
console.log('加柠檬');
}
Tea.prototype.init = function(){
this.boilWater();
this.brewTeaBag();
this.pourInCup();
this.addlemon();
}
var tea = new Tea();
tea.init();
分离出共同点
泡茶和泡咖啡的过程大同小异,主要有以下不同点:
- 原料不同。一个是咖啡,一个是茶,但我们可以把它们都抽象为饮料。
- 泡的方式不同。咖啡是冲泡,而茶叶是浸泡,我们可以把它们都抽象为“泡”。
- 加入的调料不同。一个是糖和牛奶,一个是柠檬,但我们可以把它们都抽象为“调料”。
抽象后,可以整理为下面四步:
(1)把水煮沸
(2)用沸水冲泡饮料
(3)把饮料倒进杯子
(4)加调料
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();
}
创建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('加糖和牛奶');
}
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();
上述例子中,Beverage.prototype.init
就是模板算法,Beverage.prototype.init
中封装了子类的算法框架,它作为一个算法的模板,指导子类以何种顺序去执行哪些方法。
模板方法模式的使用场景
从大的方面来讲,模板方法模式常被架构师用于搭建项目的框架,架构师订好了框架的骨架,程序员继承框架的结构之后,负责往里面填空。
在Web开发中也能找到很多模板方法模式的适用场景,比如我们在构建一系列的UI组件,这些组件的构建过程一般如下所示:
(1)初始化一个div容器;
(2)通过ajax请求拉取相应的数据;
(3)把数据渲染到div容器里面,完成组件的构造;
(4)通知用户组件渲染完毕。
我们可以看到,任何组件的构建都遵循上面的4步,其中第1步和第4步是相同的。第2步不同的地方只是请求ajax的远程地址,第3步不同的地方是渲染数据的方式。
于是我们可以把这4个步骤都抽象到父类的模板方法里面,父类中还可以顺便提供第1步和第4步的具体实现。当子类继承这个父类之后,会重写模板方法里面的第2步和第3步。
钩子方法
回顾上面冲咖啡的方法,我们已经规定好冲泡饮料的4个步骤,但是有些客人喝咖啡是不加调料的,有什么办法可以让子类不受这个约束呢?
钩子方法(hook)可以用来解决这个问题。放置钩子是隔离变化的一种常见手段。我们在父类中容易变化的地方放置钩子,钩子可以有一个默认的实现,究竟要不要“挂钩”,这由子类自行决定。钩子方法让程序拥有了变化的可能。
接下来,我们把挂钩的名字定为customerWantsCondiments,放到Beverage中:
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();
好莱坞原则
在设计中,我们允许底层组件将自己挂钩到高层组件中,而高层组件会决定什么时候、以何种方式去使用这些底层组件,这种原则被称为“好莱坞原则”。高层组件会决定什么时候、以何种方式去使用这些底层组件,高层组件对待底层组件的方式,跟演艺公司对待新人演员一样,都是“别调用我们,我们会调用你”。
模板方法模式是好莱坞原则的一个典型使用场景,它与好莱坞原则的联系非常明显,当我们用模板方法模式编写一个程序时,就意味着子类放弃了对自己的控制权,而是改为父类通知子类,哪些方法应该在什么时候被调用。作为子类,只负责提供一些设计上的细节。
除此之外,好莱坞原则还应用于其他模式和场景:
发布-订阅模式
发布者会把消息推送给订阅者,这取代了原先不断去fetch消息的形式。
回调函数
在ajax异步请求中,我们通常会把接下来的操作放在回调函数中,当数据返回后,才执行回调函数。
用好莱坞原则改写一遍上述例子,下面这段代码可以达到和继承一样的效果:
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();
模板方法模式是一种典型的通过封装变化提高系统扩展性的设计模式。我们把部分逻辑抽象到父类的模板方法里面,把部分变化的逻辑封装到子类中。通过增加新的子类,我们便能给系统增加新的功能,并不需要改动抽象父类以及其他子类,这也是符合开放-封闭原则的。