概念
??模板方法模式是一種只需使用繼承就可以實(shí)現(xiàn)的非常簡單的模式术吗。模板方法模式由兩部分結(jié)構(gòu)組成擒滑,第一部分是抽象父類骂蓖,第二部分是具體的實(shí)現(xiàn)子類积瞒。通常在抽象父類中封裝了子類的算法框架,包括實(shí)現(xiàn)一些公共方法以及封裝子類中所有方法的執(zhí)行順序登下。子類通過繼承這個抽象類茫孔,也繼承了整個算法結(jié)構(gòu),并且可以選擇重寫父類的方法被芳。
??假如有一些平行的子類缰贝,各個子類之間有一些相同的行為,也有一些不同的行為畔濒。如果相同和不同的行為都混合在各個子類的實(shí)現(xiàn)中剩晴,說明這些相同的行為會在各個子類中重復(fù)出現(xiàn)。但實(shí)際上侵状,相同的行為可以被搬移到另外一個單一的地方赞弥,模板方法模式就是為解決這個問題而生的。在模板方法模式中趣兄,子類實(shí)現(xiàn)中的相同部分被上移到父類中绽左,而將不同的部分留在子類來實(shí)現(xiàn)。這也很好地體現(xiàn)了泛化的思想艇潭。
應(yīng)用
抽象類
??模板方法模式是一種嚴(yán)重依賴抽象類的設(shè)計(jì)模式拼窥。JS在語言層面并沒有提供對抽象類的支持,故下面代碼用JS的超集TS編寫蹋凝。
??我們先來看看抽象類在正統(tǒng)的面向?qū)ο缶幊陶Z言中的作用鲁纠。在Java中,類分為兩種鳍寂,一種為具體類房交,另一種為抽象類。具體類可以被實(shí)例化伐割,抽象類不能被實(shí)例化候味。要了解抽象類不能被實(shí)例化的原因刃唤,可以思考“飲料”這個抽象類。
想象這樣一個場景:口渴了去便利店想買一瓶飲料白群,不能直接跟店員說:“來一瓶飲料”尚胞。如果這樣說了,那么店員接下來肯定會問:“要什么飲料帜慢?”飲料只是一個抽象名詞笼裳,只有當(dāng)真正明確了的飲料類型之后,才能得到一杯咖啡粱玲、茶或者可樂躬柬。
由于抽象類不能被實(shí)例化,如果有人編寫了一個抽象類抽减,那么這個抽象類一定是用來被某些具體類繼承的允青。抽象類表示一種契約。繼承了這個抽象類的所有子類都將擁有跟抽象類一致的接口方法卵沉,抽象類的主要作用就是為它的子類定義這些公共接口颠锉。如果在子類中刪掉了這些方法中的某一個,那么將不能通過編譯器的檢查史汗。
??抽象方法被聲明在抽象類中琼掠,抽象方法并沒有具體的實(shí)現(xiàn)過程,是一些“啞”方法停撞。除了抽象方法之外瓷蛙,如果每個子類中都有一些同樣的具體實(shí)現(xiàn)方法,那這些方法也可以選擇放在抽象類中戈毒,這可以節(jié)省代碼以達(dá)到復(fù)用的效果速挑,這些方法叫作具體方法。
??咖啡與茶是一個經(jīng)典的例子副硅,經(jīng)常用來講解模板方法模式姥宝,這個例子的原型來自《HeadFirst設(shè)計(jì)模式》。下面用TS模板方法模式來實(shí)現(xiàn)這個例子恐疲。
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("把咖啡倒進(jìn)杯子")
}
addCondiments():void{ //子類中重寫addCondiments方法
console.log("加糖和牛奶")
}
}
public class Tea extends Beverage{ //Tea類
brew():void{ //子類中重寫brew方法
console.log("用沸水浸泡茶葉")
}
pourInCup():void{ //子類中重寫pourInCup方法
console.log("把茶倒進(jìn)杯子")
}
addCondiments():void{ //子類中重寫addCondiments方法
console.log("加檸檬")
}
}
Beverage coffee = new Coffee() // 創(chuàng)建coffee對象
coffee.init() // 把水煮沸腊满、用沸水沖泡咖啡、把咖啡倒進(jìn)杯子培己、加糖和牛奶
Beverage tea = new Tea() // 創(chuàng)建tea對象
tea.init() // 把水煮沸碳蛋、用沸水浸泡茶葉、把茶倒進(jìn)杯子省咨、加檸檬
鉤子方法
??放置鉤子是隔離變化的一種常見手段肃弟。在父類中容易變化的地方放置鉤子,鉤子可以有一個默認(rèn)的實(shí)現(xiàn),究竟要不要“掛鉤”笤受,這由子類自行決定穷缤。鉤子方法的返回結(jié)果決定了模板方法后面部分的執(zhí)行步驟,也就是程序接下來的走向箩兽,這樣一來津肛,程序就擁有了變化的可能。
class Beverage {
init(): void {
this.boilWater()
this.brew()
this.pourInCup()
if (this.customerWantsCondiments() ){ // 如果掛鉤返回true汗贫,則需要調(diào)料
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("把咖啡倒進(jìn)杯子")
}
customerWantsCondiments() {
return window.confirm( '請問需要調(diào)料嗎身坐?' );
}
addCondiments() {
console.log( '加糖和牛奶' );
}
}
Coffee coffee = new Coffee()
coffee.init()
??下面引入一個新的設(shè)計(jì)原則——“好萊塢原則”。好萊塢無疑是演員的天堂落包,但好萊塢也有很多找不到工作的新人演員部蛇,許多新人演員在好萊塢把簡歷遞給演藝公司之后就只有回家等待電話。有時候該演員等得不耐煩了咐蝇,給演藝公司打電話詢問情況涯鲁,演藝公司往往這樣回答:“不要來找我,我會給你打電話嘹害〈楦停”
在設(shè)計(jì)中吮便,這樣的規(guī)則就稱為好萊塢原則笔呀。在這一原則的指導(dǎo)下,允許底層組件將自己掛鉤到高層組件中髓需,而高層組件會決定什么時候许师、以何種方式去使用這些底層組件,高層組件對待底層組件的方式僚匆,跟演藝公司對待新人演員一樣微渠,都是“別調(diào)用我們,我們會調(diào)用你”
模板方法模式是好萊塢原則的一個典型使用場景咧擂,它與好萊塢原則的聯(lián)系非常明顯逞盆,用模板方法模式編寫一個程序時,就意味著子類放棄了對自己的控制權(quán)松申,而是改為父類通知子類云芦,哪些方法應(yīng)該在什么時候被調(diào)用。作為子類贸桶,只負(fù)責(zé)提供一些設(shè)計(jì)上的細(xì)節(jié)舅逸。
小結(jié)
??模板方法模式是一種典型的通過封裝變化提高系統(tǒng)擴(kuò)展性的設(shè)計(jì)模式。在傳統(tǒng)的面向?qū)ο笳Z言中皇筛,一個運(yùn)用了模板方法模式的程序中琉历,子類的方法種類和執(zhí)行順序都是不變的,所以把這部分邏輯抽象到父類的模板方法里面。而子類的方法具體怎么實(shí)現(xiàn)則是可變的旗笔,于是把這部分變化的邏輯封裝到子類中彪置。通過增加新的子類,便能給系統(tǒng)增加新的功能换团,并不需要改動抽象父類以及其他子類悉稠,這也是符合開放——封閉原則的。
參考文獻(xiàn)
《JavaScript設(shè)計(jì)模式與開發(fā)實(shí)踐》