概念
模板方法模式是一种只需使用继承就可以实现的非常简单的模式。模板方法模式由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类。通常在抽象父类中封装了子类的算法框架,包括实现一些公共方法以及封装子类中所有方法的执行顺序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。
假如有一些平行的子类,各个子类之间有一些相同的行为,也有一些不同的行为。如果相同和不同的行为都混合在各个子类的实现中,说明这些相同的行为会在各个子类中重复出现。但实际上,相同的行为可以被搬移到另外一个单一的地方,模板方法模式就是为解决这个问题而生的。在模板方法模式中,子类实现中的相同部分被上移到父类中,而将不同的部分留在子类来实现。这也很好地体现了泛化的思想。
应用
抽象类
模板方法模式是一种严重依赖抽象类的设计模式。JS在语言层面并没有提供对抽象类的支持,故下面代码用JS的超集TS编写。
我们先来看看抽象类在正统的面向对象编程语言中的作用。在Java中,类分为两种,一种为具体类,另一种为抽象类。具体类可以被实例化,抽象类不能被实例化。要了解抽象类不能被实例化的原因,可以思考“饮料”这个抽象类。
想象这样一个场景:口渴了去便利店想买一瓶饮料,不能直接跟店员说:“来一瓶饮料”。如果这样说了,那么店员接下来肯定会问:“要什么饮料?”饮料只是一个抽象名词,只有当真正明确了的饮料类型之后,才能得到一杯咖啡、茶或者可乐。
由于抽象类不能被实例化,如果有人编写了一个抽象类,那么这个抽象类一定是用来被某些具体类继承的。抽象类表示一种契约。继承了这个抽象类的所有子类都将拥有跟抽象类一致的接口方法,抽象类的主要作用就是为它的子类定义这些公共接口。如果在子类中删掉了这些方法中的某一个,那么将不能通过编译器的检查。
抽象方法被声明在抽象类中,抽象方法并没有具体的实现过程,是一些“哑”方法。除了抽象方法之外,如果每个子类中都有一些同样的具体实现方法,那这些方法也可以选择放在抽象类中,这可以节省代码以达到复用的效果,这些方法叫作具体方法。
咖啡与茶是一个经典的例子,经常用来讲解模板方法模式,这个例子的原型来自《HeadFirst设计模式》。下面用TS模板方法模式来实现这个例子。
public abstract class Beverage{ //饮料抽象类
init():void{ //模板方法
this.boilWater()
this.brew()
this.pourInCup()
this.addCondiments()
}
boilWater():void{ //具体方法
console.log("把水煮沸")
}
abstract brew():void //抽象方法brew
abstract addCondiments():void //抽象方法addCondiments
abstract pourInCup():void //抽象方法pourInCup
}
public class Coffee extends Beverage{ //Coffee类
brew():void{ //子类中重写brew方法
console.log("用沸水冲泡咖啡")
}
pourInCup():void{ //子类中重写pourInCup方法
console.log("把咖啡倒进杯子")
}
addCondiments():void{ //子类中重写addCondiments方法
console.log("加糖和牛奶")
}
}
public class Tea extends Beverage{ //Tea类
brew():void{ //子类中重写brew方法
console.log("用沸水浸泡茶叶")
}
pourInCup():void{ //子类中重写pourInCup方法
console.log("把茶倒进杯子")
}
addCondiments():void{ //子类中重写addCondiments方法
console.log("加柠檬")
}
}
Beverage coffee = new Coffee() // 创建coffee对象
coffee.init() // 把水煮沸、用沸水冲泡咖啡、把咖啡倒进杯子、加糖和牛奶
Beverage tea = new Tea() // 创建tea对象
tea.init() // 把水煮沸、用沸水浸泡茶叶、把茶倒进杯子、加柠檬
钩子方法
放置钩子是隔离变化的一种常见手段。在父类中容易变化的地方放置钩子,钩子可以有一个默认的实现,究竟要不要“挂钩”,这由子类自行决定。钩子方法的返回结果决定了模板方法后面部分的执行步骤,也就是程序接下来的走向,这样一来,程序就拥有了变化的可能。
class Beverage {
init(): void {
this.boilWater()
this.brew()
this.pourInCup()
if (this.customerWantsCondiments() ){ // 如果挂钩返回true,则需要调料
this.addCondiments();
}
}
boilWater(): void {
console.log("把水煮沸")
}
brew() {
throw new Error('子类必须重写brew方法')
}
pourInCup() {
throw new Error('子类必须重写pourInCup方法')
}
customerWantsCondiments ():boolean {
return true
}
addCondiments() {
throw new Error('子类必须重写addCondiments方法')
}
}
class Coffee extends Beverage {
brew() {
console.log("用沸水冲泡咖啡")
}
pourInCup(){
console.log("把咖啡倒进杯子")
}
customerWantsCondiments() {
return window.confirm( '请问需要调料吗?' );
}
addCondiments() {
console.log( '加糖和牛奶' );
}
}
Coffee coffee = new Coffee()
coffee.init()
下面引入一个新的设计原则——“好莱坞原则”。好莱坞无疑是演员的天堂,但好莱坞也有很多找不到工作的新人演员,许多新人演员在好莱坞把简历递给演艺公司之后就只有回家等待电话。有时候该演员等得不耐烦了,给演艺公司打电话询问情况,演艺公司往往这样回答:“不要来找我,我会给你打电话。”
在设计中,这样的规则就称为好莱坞原则。在这一原则的指导下,允许底层组件将自己挂钩到高层组件中,而高层组件会决定什么时候、以何种方式去使用这些底层组件,高层组件对待底层组件的方式,跟演艺公司对待新人演员一样,都是“别调用我们,我们会调用你”
模板方法模式是好莱坞原则的一个典型使用场景,它与好莱坞原则的联系非常明显,用模板方法模式编写一个程序时,就意味着子类放弃了对自己的控制权,而是改为父类通知子类,哪些方法应该在什么时候被调用。作为子类,只负责提供一些设计上的细节。
小结
模板方法模式是一种典型的通过封装变化提高系统扩展性的设计模式。在传统的面向对象语言中,一个运用了模板方法模式的程序中,子类的方法种类和执行顺序都是不变的,所以把这部分逻辑抽象到父类的模板方法里面。而子类的方法具体怎么实现则是可变的,于是把这部分变化的逻辑封装到子类中。通过增加新的子类,便能给系统增加新的功能,并不需要改动抽象父类以及其他子类,这也是符合开放——封闭原则的。
参考文献
《JavaScript设计模式与开发实践》