模版方法模式
模版方法是一種只需使用繼承就可以實現(xiàn)的非常簡單的模式
模版方法模式由兩部分結(jié)構(gòu)組成,第一部分是抽象父類,第二部分是具體的實現(xiàn)子類.通常在抽象父類中封裝了子類的算法框架,包括實現(xiàn)一些公共方法以及封裝子類中所有方法的執(zhí)行順序.子類通過繼承這個抽象類,也繼承了整個算法結(jié)構(gòu),并且可以選擇重寫父類的方法
模式作用:
- 一次性實現(xiàn)一個算法的不變的部分,并將可變的行為留給子類來實現(xiàn)
- 各子類中公共的行為應(yīng)被提取出來并集中到一個公共父類中,避免代碼重復(fù),不同之處分離為新的操作,最后用一個鉤子的模版方法來替換這些不同的代碼
- 控制子類擴展,模版方法只在特定點調(diào)用"hook"操作,這樣就允許在這些點進行擴展
注意事項
- 和策略模式不同,模版方法使用繼承來改變算法的一部分,而策略模式使用委托來改變整個算法
例子
Coffee of Tea
咖啡與茶是一個經(jīng)典的例子,經(jīng)常用來講解模版方法模式
先泡一杯咖啡
首先,我們先來泡一杯咖啡,如果沒有什么太個性化的的需求,泡咖啡的步驟通常如下:
(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();
泡一壺茶
接下來,開始準備我們的茶,泡茶的步驟跟泡咖啡的步驟相差并不大:
(1) 把水煮沸
(2) 用沸水浸泡茶葉
(3) 把茶水倒進杯子
(4) 加檸檬
同樣用一段代碼來實現(xiàn)泡茶的步驟:
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();
分類出共同點
現(xiàn)在我們分別泡好了一杯咖啡和一壺茶,經(jīng)過思考和比較,我們發(fā)現(xiàn)咖啡和茶的沖泡過程是大同小異的.
我們找到泡咖啡和泡茶主要有以下不同點
- 原料不同.一個是咖啡,一個是茶,但我們可以把它們抽象為"飲料"
- 泡的方式不同.咖啡是沖泡,而茶葉是浸泡,我們可以把它們都抽象為"泡"
- 加入的調(diào)料不同.一個是糖和牛奶,一個是檸檬,但我們可以把它們都抽象為"調(diào)料"
經(jīng)過抽象之后,不管是泡咖啡還是泡茶,我們都能整理為下面四步:
(1) 把水煮沸
(2) 用沸水沖泡飲料
(3) 把飲料倒進杯子
(4) 加調(diào)料
所以,不管是沖泡還是浸泡,我們都能給它一個新的方法名稱,比如說brew()
.同理,不管是加糖和牛奶,還是加檸檬,我們都可以稱之為addCoundiments()
讓我們忘記最開始創(chuàng)建的Coffee和Tea類.現(xiàn)在可以創(chuàng)建一個抽象父類來表示泡一杯飲料的整個過程.不論是Coffee還是Tea,都被我們用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.init=function(){
this.boilWater();
this.brew();
this.pourInCup();
this.addCondiments();
}
創(chuàng)建Coffee子類和Tea子類
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()
現(xiàn)在我們的Coffee類已經(jīng)完成了,接下來依葫蘆畫瓢,創(chuàng)建我們的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.prptotype.init
Beverage.prptotype.init
被稱為模版方法的原因是,該方法封裝了子類的算法框架,它作為一個算法的模版,指導子類以何種順序去執(zhí)行哪些方法.在Beverage.prptotype.init
方法中,算法內(nèi)的每一個步驟都清楚地展示在我們眼前.
鉤子方法
通過模版方法模式,我們在父類中封裝了子類的算法框架.這些算法框架在正常狀態(tài)下適用于大多數(shù)子類的,但如果有一些特別"個性"的子類呢?比如我們在飲料類Beverage中封裝了飲料的沖泡順序:
(1) 把水煮沸
(2) 用沸水沖泡飲料
(3) 把飲料倒進杯子
(4) 加調(diào)料
這四個沖泡飲料的步驟適用于咖啡和茶,在我們的飲料店里,根據(jù)這4個步驟制作出來的咖啡和茶,一直順利地提供給絕大部分客人享用.但有一些客人喝咖啡是不加調(diào)料的(糖和牛奶)的.既然Bverage作為父類,已經(jīng)規(guī)定好了沖泡飲料的4個步驟,那么有什么辦法可以讓子類不受這個約束呢?
鉤子方法(hook)可以用來解決這個問題,放置鉤子是隔離變化的一種常見手段.我們在父類中容易變化的地方放置鉤子,鉤子可以有一個默認的實現(xiàn),究竟要不要"掛鉤",這由子類自行決定.鉤子方法的返回結(jié)構(gòu)決定了模版方法后面的執(zhí)行步驟,也就是程序接下來的走向,這樣一來,程序就擁有了變化的可能.
在這個例子里,我們把掛鉤的名字定位customerWantsCondiments,接下來將掛鉤放入Beverage類,看看我們?nèi)绾蔚玫揭槐恍枰呛团D痰目Х?代碼如下:
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()){
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("加糖和牛奶");
}
Beverage.prototype.customerWantsCondiments=function(){
return window.confirm("請問要加調(diào)料嗎?");
}
var coffee=new Coffee();
coffee.init()
在JavaScript中,我們很多時候不需要依樣畫瓢地實現(xiàn)一個模版方法模式,高階函數(shù)是更好的選擇.