模板方法模式是一種只需使用繼承就可以實現(xiàn)的非常簡單的模式。
模板方法模式由兩部分結(jié)構(gòu)組成,第一部分是抽象父類,第二部分是具體的實現(xiàn)子類。通常在抽象父類中封裝了子類的算法框架,包括實現(xiàn)一些公共方法以及封裝子類中所有方法的執(zhí)行順序。子類通過繼承這個抽象類,也繼承了整個算法結(jié)構(gòu),并且可以選擇重寫父類的方法。
假如我們有一些平行的子類,各個子類之間有一些相同的行為,也有一些不同的行為。如果相同和不同的行為都混合在各個子類的實現(xiàn)中,說明這些相同的行為會在各個子類中重復(fù)出現(xiàn)。但實際上,相同的行為可以被搬移到另外一個單一的地方,模板方法模式就是為解決這個問題而生的。在模板方法模式中,子類實現(xiàn)中的相同部分被上移到父類中,而將不同的部分留待子類來實現(xiàn)。這也很好地體現(xiàn)了泛化的思想。
基于繼承實現(xiàn)一個模板方法模式
const 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()
}
}
const 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('請問需要調(diào)料嗎?')
}
let coffeeWithHook=new CoffeeWithHook()
coffeeWithHook.init()
其中的customerWantsCondiments是鉤子方法,子類可以通過自己定義此方法來控制父類的行為,當(dāng)然這個鉤子需要時父類預(yù)設(shè)好的。
好萊塢原則
好萊塢無疑是演員的天堂,但好萊塢也有很多找不到工作的新人演員,許多新人演員在好萊塢把簡歷遞給演藝公司之后就只有回家等待電話。有時候該演員等得不耐煩了,給演藝公司打電話詢問情況,演藝公司往往這樣回答:“不要來找我,我會給你打電話。”
在設(shè)計中,這樣的規(guī)則就稱為好萊塢原則。在這一原則的指導(dǎo)下,我們允許底層組件將自己掛鉤到高層組件中,而高層組件會決定什么時候、以何種方式去使用這些底層組件,高層組件對待底層組件的方式,跟演藝公司對待新人演員一樣,都是“別調(diào)用我們,我們會調(diào)用你”。
模板方法模式是好萊塢原則的一個典型使用場景,它與好萊塢原則的聯(lián)系非常明顯,當(dāng)我們用模板方法模式編寫一個程序時,就意味著子類放棄了對自己的控制權(quán),而是改為父類通知子類,哪些方法應(yīng)該在什么時候被調(diào)用。作為子類,只負責(zé)提供一些設(shè)計上的細節(jié)。
除此之外,好萊塢原則還常常應(yīng)用于其他模式和場景,例如發(fā)布-訂閱模式和回調(diào)函數(shù)。
發(fā)布—訂閱模式
在發(fā)布—訂閱模式中,發(fā)布者會把消息推送給訂閱者,這取代了原先不斷去fetch消息的形式。例如假設(shè)我們乘坐出租車去一個不了解的地方,除了每過5秒鐘就問司機“是否到達目的地”之外,還可以在車上美美地睡上一覺,然后跟司機說好,等目的地到了就叫醒你。這也相當(dāng)于好萊塢原則中提到的“別調(diào)用我們,我們會調(diào)用你”。回調(diào)函數(shù)
在ajax異步請求中,由于不知道請求返回的具體時間,而通過輪詢?nèi)ヅ袛嗍欠穹祷財?shù)據(jù),這顯然是不理智的行為。所以我們通常會把接下來的操作放在回調(diào)函數(shù)中,傳入發(fā)起ajax異步請求的函數(shù)。當(dāng)數(shù)據(jù)返回之后,這個回調(diào)函數(shù)才被執(zhí)行,這也是好萊塢原則的一種體現(xiàn)。把需要執(zhí)行的操作封裝在回調(diào)函數(shù)里,然后把主動權(quán)交給另外一個函數(shù)。至于回調(diào)函數(shù)什么時候被執(zhí)行,則是另外一個函數(shù)控制的。
javascript高階函數(shù)實現(xiàn)模板方法模式
const Beverage=function(){
const boilWater=function(){
console.log('把水煮沸')
}
const brew=params.brew||function(){
throw new Error('必須傳遞brew方法')
}
const pourInCup=parasm.pourInCup||function(){
throw new Error('必須傳遞pourInCup方法')
}
const addCondiments=params.addCondiments||function(){
throw new Error('必須傳遞addCondiments方法')
}
const F=function(){}
F.prototype.init=function(){
boilWater()
brew()
pourInCup()
addCondiments()
}
return F
}
const Coffee=Beverage({
brew:function(){
console.log('用沸水沖泡咖啡')
},
pourInCup:function(){
console.log('把咖啡倒進杯子')
},
addCondiments:function(){
console.log('加糖和牛奶')
}
})
const Tea=Beverage({
brew:function(){
console.log('用沸水沖泡茶葉')
},
pourInCup:function(){
console.log('把茶倒進杯子')
},
addCondiments:function(){
console.log('加檸檬')
}
})
const coffee=new Coffee()
coffee.init()
const tea=new Tea()
tea.init()
模板方法模式是一種典型的通過封裝變化提高系統(tǒng)擴展性的設(shè)計模式。在傳統(tǒng)的面向?qū)ο笳Z言中,一個運用了模板方法模式的程序中,子類的方法種類和執(zhí)行順序都是不變的,所以我們把這部分邏輯抽象到父類的模板方法里面。而子類的方法具體怎么實現(xiàn)則是可變的,于是我們把這部分變化的邏輯封裝到子類中。通過增加新的子類,我們便能給系統(tǒng)增加新的功能,并不需要改動抽象父類以及其他子類,這也是符合開放-封閉原則的。
但在JavaScript中,我們很多時候都不需要依樣畫瓢地去實現(xiàn)一個模版方法模式,高階函數(shù)是更好的選擇。