在JavaScript開發(fā)中用到繼承的場(chǎng)景其實(shí)并不是很多,很多時(shí)候我們都喜歡用mix-in的方式給對(duì)象擴(kuò)展屬性。但這不代表繼承在JavaScript里沒有用武之地,雖然沒有真正的類和繼承機(jī)制,但我們可以通過原型prototype來變相地實(shí)現(xiàn)繼承。
不過這里并非要討論繼承,而是討論一種基于繼承的設(shè)計(jì)模式——模板方法(Template Method)模式。
模板方法模式的定義和組成
模板方法模式是一種只需使用繼承就可以實(shí)現(xiàn)的非常簡(jiǎn)單的模式。
模板方法模式由兩部分結(jié)構(gòu)組成,第一部分是抽象父類,第二部分是具體的實(shí)現(xiàn)子類。通常在抽象父類中封裝了子類的算法框架,包括實(shí)現(xiàn)一些公共方法以及封裝子類中所有方法的執(zhí)行順序。子類通過繼承這個(gè)抽象類,也繼承了整個(gè)算法結(jié)構(gòu),并且可以選擇重寫父類的方法。
假如我們有一些平行的子類,各個(gè)子類之間有一些相同的行為,也有一些不同的行為。如果相同和不同的行為都混合在各個(gè)子類的實(shí)現(xiàn)中,說明這些相同的行為會(huì)在各個(gè)子類中重復(fù)出現(xiàn)。但實(shí)際上,相同的行為可以被搬移到另外一個(gè)單一的地方,模板方法模式就是為解決這個(gè)問題而生的。在模板方法模式中,子類實(shí)現(xiàn)中的相同部分被上移到父類中,而將不同的部分留待子類來實(shí)現(xiàn)。這也很好地體現(xiàn)了泛化的思想。
第一個(gè)例子——Coffee or Tea
咖啡與茶是一個(gè)經(jīng)典的例子,經(jīng)常用來講解模板方法模式,這個(gè)例子的原型來自《Head First設(shè)計(jì)模式》。這里我們就用JavaScript來實(shí)現(xiàn)這個(gè)例子。
1.先泡一杯咖啡
首先,我們先來泡一杯咖啡,如果沒有什么太個(gè)性化的需求,泡咖啡的步驟通常如下:
(1) 把水煮沸
(2) 用沸水沖泡咖啡
(3) 把咖啡倒進(jìn)杯子
(4) 加糖和牛奶
通過下面這段代碼,我們就能得到一杯香濃的咖啡:
var Coffee = function(){};
Coffee.prototype.boilWater = function(){
console.log( '把水煮沸' );
};
Coffee.prototype.brewCoffeeGriends = function(){
console.log( '用沸水沖泡咖啡' );
};
Coffee.prototype.pourInCup = function(){
console.log( '把咖啡倒進(jìn)杯子' );
};
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.泡一壺茶
接下來,開始準(zhǔn)備我們的茶,泡茶的步驟跟泡咖啡的步驟相差并不大:
(1) 把水煮沸
(2) 用沸水浸泡茶葉
(3) 把茶水倒進(jìn)杯子
(4) 加檸檬
同樣用一段代碼來實(shí)現(xiàn)泡茶的步驟:
var Tea = function(){};
Tea.prototype.boilWater = function(){
console.log( '把水煮沸' );
};
Tea.prototype.steepTeaBag = function(){
console.log( '用沸水浸泡茶葉' );
};
Tea.prototype.pourInCup = function(){
console.log( '把茶水倒進(jìn)杯子' );
};
Tea.prototype.addLemon = function(){
console.log( '加檸檬' );
};
Tea.prototype.init = function(){
this.boilWater();
this.steepTeaBag();
this.pourInCup();
this.addLemon();
};
var tea = new Tea();
tea.init();
3.分離出共同點(diǎn)
現(xiàn)在我們分別泡好了一杯咖啡和一壺茶,經(jīng)過思考和比較,我們發(fā)現(xiàn)咖啡和茶的沖泡過程是大同小異的。
我們找到泡咖啡和泡茶主要有以下不同點(diǎn)。
原料不同。一個(gè)是咖啡,一個(gè)是茶,但我們可以把它們都抽象為“飲料”。
泡的方式不同。咖啡是沖泡,而茶葉是浸泡,我們可以把它們都抽象為“泡”。
加入的調(diào)料不同。一個(gè)是糖和牛奶,一個(gè)是檸檬,但我們可以把它們都抽象為“調(diào)料”。
經(jīng)過抽象之后,不管是泡咖啡還是泡茶,我們都能整理為下面四步:
(1) 把水煮沸
(2) 用沸水沖泡飲料
(3) 把飲料倒進(jìn)杯子
(4) 加調(diào)料
所以,不管是沖泡還是浸泡,我們都能給它一個(gè)新的方法名稱,比如說brew()。同理,不管是加糖和牛奶,還是加檸檬,我們都可以稱之為addCondiments()。
讓我們忘記最開始創(chuàng)建的Coffee類和Tea類。 現(xiàn)在可以創(chuàng)建一個(gè)抽象父類來表示泡一杯飲料的整個(gè)過程。不論是Coffee,還是Tea,都被我們用Beverage來表示,代碼如下:
var Beverage = function(){};
Beverage.prototype.boilWater = function(){
console.log( '把水煮沸' );
};
Beverage.prototype.brew = function(){}; // 空方法,應(yīng)該由子類重寫
Beverage.prototype.pourInCup = function(){}; // 空方法,應(yīng)該由子類重寫
Beverage.prototype.addCondiments = function(){}; // 空方法,應(yīng)該由子類重寫
Beverage.prototype.init = function(){
this.boilWater();
this.brew();
this.pourInCup();
this.addCondiments();
};
4.創(chuàng)建Coffee子類和Tea子類
現(xiàn)在創(chuàng)建一個(gè)Beverage類的對(duì)象對(duì)我們來說沒有意義,因?yàn)槭澜缟夏芎鹊臇|西沒有一種真正叫“飲料”的,飲料在這里還只是一個(gè)抽象的存在。接下來我們要?jiǎng)?chuàng)建咖啡類和茶類,并讓它們繼承飲料類:
var Coffee = function(){};
Coffee.prototype = new Beverage();
接下來要重寫抽象父類中的一些方法,只有“把水煮沸”這個(gè)行為可以直接使用父類Beverage中的boilWater方法,其他方法都需要在Coffee子類中重寫,代碼如下:
Coffee.prototype.brew = function(){
console.log( '用沸水沖泡咖啡' );
};
Coffee.prototype.pourInCup = function(){
console.log( '把咖啡倒進(jìn)杯子' );
};
Coffee.prototype.addCondiments = function(){
console.log( '加糖和牛奶' );
};
var Coffee = new Coffee();
Coffee.init();
至此我們的Coffee類已經(jīng)完成了,當(dāng)調(diào)用coffee對(duì)象的init方法時(shí),由于coffee對(duì)象和Coffee構(gòu)造器的原型prototype上都沒有對(duì)應(yīng)的init方法,所以該請(qǐng)求會(huì)順著原型鏈,被委托給Coffee的“父類”Beverage原型上的init方法。
而Beverage.prototype.init方法中已經(jīng)規(guī)定好了泡飲料的順序,所以我們能成功地泡出一杯咖啡,代碼如下:
Beverage.prototype.init = function(){
this.boilWater();
this.brew();
this.pourInCup();
this.addCondiments();
};
接下來照葫蘆畫瓢,來創(chuàng)建我們的Tea類:
var Tea = function(){};
Tea.prototype = new Beverage();
Tea.prototype.brew = function(){
console.log( '用沸水浸泡茶葉' );
};
Tea.prototype.pourInCup = function(){
console.log( '把茶倒進(jìn)杯子' );
};
Tea.prototype.addCondiments = function(){
console.log( '加檸檬' );
};
var tea = new Tea();
tea.init();
這里一直討論的是模板方法模式,那么在上面的例子中,到底誰才是所謂的模板方法呢?答案是Beverage.prototype.init。
Beverage.prototype.init被稱為模板方法的原因是,該方法中封裝了子類的算法框架,它作為一個(gè)算法的模板,指導(dǎo)子類以何種順序去執(zhí)行哪些方法。在Beverage.prototype.init方法中,算法內(nèi)的每一個(gè)步驟都清楚地展示在我們眼前。
抽象類
首先要說明的是,模板方法模式是一種嚴(yán)重依賴抽象類的設(shè)計(jì)模式。JavaScript在語言層面并沒有提供對(duì)抽象類的支持,我們也很難模擬抽象類的實(shí)現(xiàn)。這里我們將著重討論Java中抽象類的作用,以及JavaScript沒有抽象類時(shí)所做出的讓步和變通。
1.抽象類的作用
在Java中,類分為兩種,一種為具體類,另一種為抽象類。具體類可以被實(shí)例化,抽象類不能被實(shí)例化。要了解抽象類不能被實(shí)例化的原因,我們可以思考“飲料”這個(gè)抽象類。
想象這樣一個(gè)場(chǎng)景:我們口渴了,去便利店想買一瓶飲料,我們不能直接跟店員說:“來一瓶飲料?!比绻覀冞@樣說了,那么店員接下來肯定會(huì)問:“要什么飲料?”飲料只是一個(gè)抽象名詞,只有當(dāng)我們真正明確了的飲料類型之后,才能得到一杯咖啡、茶、或者可樂。
由于抽象類不能被實(shí)例化,如果有人編寫了一個(gè)抽象類,那么這個(gè)抽象類一定是用來被某些具體類繼承的。
抽象類和接口一樣可以用于向上轉(zhuǎn)型(可參考1.3節(jié)關(guān)于多態(tài)的內(nèi)容),在靜態(tài)類型語言中,編譯器對(duì)類型的檢查總是一個(gè)繞不過的話題與困擾。雖然類型檢查可以提高程序的安全性,但繁瑣而嚴(yán)格的類型檢查也時(shí)常會(huì)讓程序員覺得麻煩。把對(duì)象的真正類型隱藏在抽象類或者接口之后,這些對(duì)象才可以被互相替換使用。這可以讓我們的Java程序盡量遵守依賴倒置原則。
除了用于向上轉(zhuǎn)型,抽象類也可以表示一種契約。繼承了這個(gè)抽象類的所有子類都將擁有跟抽象類一致的接口方法,抽象類的主要作用就是為它的子類定義這些公共接口。如果我們?cè)谧宇愔袆h掉了這些方法中的某一個(gè),那么將不能通過編譯器的檢查,這在某些場(chǎng)景下是非常有用的,比如我們本章討論的模板方法模式,Beverage類的init方法里規(guī)定了沖泡一杯飲料的順序如下:
this.boilWater(); // 把水煮沸
this.brew(); // 用水泡原料
this.pourInCup(); // 把原料倒進(jìn)杯子
this.addCondiments(); // 添加調(diào)料
如果在Coffee子類中沒有實(shí)現(xiàn)對(duì)應(yīng)的brew方法,那么我們百分之百得不到一杯咖啡。既然父類規(guī)定了子類的方法和執(zhí)行這些方法的順序,子類就應(yīng)該擁有這些方法,并且提供正確的實(shí)現(xiàn)。
抽象方法和具體方法
抽象方法被聲明在抽象類中,抽象方法并沒有具體的實(shí)現(xiàn)過程,是一些“啞”方法。比如Beverage類中的brew方法、pourInCup方法和addCondiments方法,都被聲明為抽象方法。當(dāng)子類繼承了這個(gè)抽象類時(shí),必須重寫父類的抽象方法。
除了抽象方法之外,如果每個(gè)子類中都有一些同樣的具體實(shí)現(xiàn)方法,那這些方法也可以選擇放在抽象類中,這可以節(jié)省代碼以達(dá)到復(fù)用的效果,這些方法叫作具體方法。當(dāng)代碼需要改變時(shí),我們只需要改動(dòng)抽象類里的具體方法就可以了。比如飲料中的boilWater方法,假設(shè)沖泡所有的飲料之前,都要先把水煮沸,那我們自然可以把boilWater方法放在抽象類Beverage中。
用Java實(shí)現(xiàn)Coffee or Tea的例子
下面我們嘗試著把Coffee和Tea的例子換成Java代碼,這有助于我們理解抽象類的意義。
// Java代碼
public abstract class Beverage { // 飲料抽象類
final void init(){ // 模板方法
boilWater();
brew();
pourInCup();
addCondiments();
}
void boilWater(){ // 具體方法boilWater
System.out.println( "把水煮沸" );
}
abstract void brew(); // 抽象方法brew
abstract void addCondiments(); // 抽象方法addCondiments
abstract void pourInCup(); // 抽象方法pourInCup
}
public class Coffee extends Beverage{ // Coffee類
@Override
void brew() { // 子類中重寫brew方法
System.out.println( "用沸水沖泡咖啡" );
}
@Override
void pourInCup(){ // 子類中重寫pourInCup方法
System.out.println( "把咖啡倒進(jìn)杯子" );
}
@Override
void addCondiments() { // 子類中重寫addCondiments方法
System.out.println( "加糖和牛奶" );
}
}
public class Tea extends Beverage{ // Tea類
@Override
void brew() { // 子類中重寫brew方法
System.out.println( "用沸水浸泡茶葉" );
}
@Override
void pourInCup(){ // 子類中重寫pourInCup方法
System.out.println( "把茶倒進(jìn)杯子" );
}
@Override
void addCondiments() { // 子類中重寫addCondiments方法
System.out.println( "加檸檬" );
}
}
public class Test {
private static void prepareRecipe( Beverage beverage ){
beverage.init();
}
public static void main( String args[] ){
Beverage coffee = new Coffee(); // 創(chuàng)建coffee對(duì)象
prepareRecipe( coffee ); // 開始泡咖啡
// 把水煮沸
// 用沸水沖泡咖啡
// 把咖啡倒進(jìn)杯子
// 加糖和牛奶
Beverage tea = new Tea(); // 創(chuàng)建tea對(duì)象
prepareRecipe( tea ); // 開始泡茶
// 把水煮沸
// 用沸水浸泡茶葉
// 把茶倒進(jìn)杯子
// 加檸檬
}
}
JavaScript沒有抽象類的缺點(diǎn)和解決方案
JavaScript并沒有從語法層面提供對(duì)抽象類的支持。抽象類的第一個(gè)作用是隱藏對(duì)象的具體類型,由于JavaScript是一門“類型模糊”的語言,所以隱藏對(duì)象的類型在JavaScript中并不重要。
另一方面, 當(dāng)我們?cè)贘avaScript中使用原型繼承來模擬傳統(tǒng)的類式繼承時(shí),并沒有編譯器幫助我們進(jìn)行任何形式的檢查,我們也沒有辦法保證子類會(huì)重寫父類中的“抽象方法”。
我們知道,Beverage.prototype.init方法作為模板方法,已經(jīng)規(guī)定了子類的算法框架,代碼如下:
Beverage.prototype.init = function(){
this.boilWater();
this.brew();
this.pourInCup();
this.addCondiments();
};
如果我們的Coffee類或者Tea類忘記實(shí)現(xiàn)這4個(gè)方法中的一個(gè)呢?拿brew方法舉例,如果我們忘記編寫Coffee.prototype.brew方法,那么當(dāng)請(qǐng)求coffee對(duì)象的brew時(shí),請(qǐng)求會(huì)順著原型鏈找到Beverage“父類”對(duì)應(yīng)的Beverage.prototype.brew方法,而Beverage.prototype.brew方法到目前為止是一個(gè)空方法,這顯然是不能符合我們需要的。
在Java中編譯器會(huì)保證子類會(huì)重寫父類中的抽象方法,但在JavaScript中卻沒有進(jìn)行這些檢查工作。我們?cè)诰帉懘a的時(shí)候得不到任何形式的警告,完全寄托于程序員的記憶力和自覺性是很危險(xiǎn)的,特別是當(dāng)我們使用模板方法模式這種完全依賴?yán)^承而實(shí)現(xiàn)的設(shè)計(jì)模式時(shí)。
下面提供兩種變通的解決方案。
第1種方案是用鴨子類型來模擬接口檢查,以便確保子類中確實(shí)重寫了父類的方法。但模擬接口檢查會(huì)帶來不必要的復(fù)雜性,而且要求程序員主動(dòng)進(jìn)行這些接口檢查,這就要求我們?cè)跇I(yè)務(wù)代碼中添加一些跟業(yè)務(wù)邏輯無關(guān)的代碼。
第2種方案是讓Beverage.prototype.brew等方法直接拋出一個(gè)異常,如果因?yàn)榇中耐浘帉慍offee.prototype.brew方法,那么至少我們會(huì)在程序運(yùn)行時(shí)得到一個(gè)錯(cuò)誤:
Beverage.prototype.brew = function(){
throw new Error( '子類必須重寫brew方法' );
};
Beverage.prototype.pourInCup = function(){
throw new Error( '子類必須重寫pourInCup方法' );
};
Beverage.prototype.addCondiments = function(){
throw new Error( '子類必須重寫addCondiments方法' );
};
第2種解決方案的優(yōu)點(diǎn)是實(shí)現(xiàn)簡(jiǎn)單,付出的額外代價(jià)很少;缺點(diǎn)是我們得到錯(cuò)誤信息的時(shí)間點(diǎn)太靠后。
我們一共有3次機(jī)會(huì)得到這個(gè)錯(cuò)誤信息,第1次是在編寫代碼的時(shí)候,通過編譯器的檢查來得到錯(cuò)誤信息;第2次是在創(chuàng)建對(duì)象的時(shí)候用鴨子類型來進(jìn)行“接口檢查”;而目前我們不得不利用最后一次機(jī)會(huì),在程序運(yùn)行過程中才知道哪里發(fā)生了錯(cuò)誤。
模板方法模式的使用場(chǎng)景
從大的方面來講,模板方法模式常被架構(gòu)師用于搭建項(xiàng)目的框架,架構(gòu)師定好了框架的骨架,程序員繼承框架的結(jié)構(gòu)之后,負(fù)責(zé)往里面填空,比如Java程序員大多使用過HttpServlet技術(shù)來開發(fā)項(xiàng)目。
一個(gè)基于HttpServlet的程序包含7個(gè)生命周期,這7個(gè)生命周期分別對(duì)應(yīng)一個(gè)do方法。
doGet()
doHead()
doPost()
doPut()
doDelete()
doOption()
doTrace()
HttpServlet類還提供了一個(gè)service方法,它就是這里的模板方法,service規(guī)定了這些do方法的執(zhí)行順序,而這些do方法的具體實(shí)現(xiàn)則需要HttpServlet的子類來提供。
在Web開發(fā)中也能找到很多模板方法模式的適用場(chǎng)景,比如我們?cè)跇?gòu)建一系列的UI組件,這些組件的構(gòu)建過程一般如下所示:
(1) 初始化一個(gè)div容器;
(2) 通過ajax請(qǐng)求拉取相應(yīng)的數(shù)據(jù);
(3) 把數(shù)據(jù)渲染到div容器里面,完成組件的構(gòu)造;
(4) 通知用戶組件渲染完畢。
我們看到,任何組件的構(gòu)建都遵循上面的4步,其中第(1)步和第(4)步是相同的。第(2)步不同的地方只是請(qǐng)求ajax的遠(yuǎn)程地址,第(3)步不同的地方是渲染數(shù)據(jù)的方式。
于是我們可以把這4個(gè)步驟都抽象到父類的模板方法里面,父類中還可以順便提供第(1)步和第(4)步的具體實(shí)現(xiàn)。當(dāng)子類繼承這個(gè)父類之后,會(huì)重寫模板方法里面的第(2)步和第(3)步。
鉤子方法
通過模板方法模式,我們?cè)诟割愔蟹庋b了子類的算法框架。這些算法框架在正常狀態(tài)下是適用于大多數(shù)子類的,但如果有一些特別“個(gè)性”的子類呢?比如我們?cè)陲嬃项怋everage中封裝了飲料的沖泡順序:
(1) 把水煮沸
(2) 用沸水沖泡飲料
(3) 把飲料倒進(jìn)杯子
(4) 加調(diào)料
這4個(gè)沖泡飲料的步驟適用于咖啡和茶,在我們的飲料店里,根據(jù)這4個(gè)步驟制作出來的咖啡和茶,一直順利地提供給絕大部分客人享用。但有一些客人喝咖啡是不加調(diào)料(糖和牛奶)的。既然Beverage作為父類,已經(jīng)規(guī)定好了沖泡飲料的4個(gè)步驟,那么有什么辦法可以讓子類不受這個(gè)約束呢?
鉤子方法(hook)可以用來解決這個(gè)問題,放置鉤子是隔離變化的一種常見手段。我們?cè)诟割愔腥菀鬃兓牡胤椒胖勉^子,鉤子可以有一個(gè)默認(rèn)的實(shí)現(xiàn),究竟要不要“掛鉤”,這由子類自行決定。鉤子方法的返回結(jié)果決定了模板方法后面部分的執(zhí)行步驟,也就是程序接下來的走向,這樣一來,程序就擁有了變化的可能。
在這個(gè)例子里,我們把掛鉤的名字定為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; // 默認(rèn)需要調(diào)料
};
Beverage.prototype.init = function(){
this.boilWater();
this.brew();
this.pourInCup();
if ( this.customerWantsCondiments() ){ // 如果掛鉤返回true,則需要調(diào)料
this.addCondiments();
}
};
var CoffeeWithHook = function(){};
CoffeeWithHook.prototype = new Beverage();
CoffeeWithHook.prototype.brew = function(){
console.log( '用沸水沖泡咖啡' );
};
CoffeeWithHook.prototype.pourInCup = function(){
console.log( '把咖啡倒進(jìn)杯子' );
};
CoffeeWithHook.prototype.addCondiments = function(){
console.log( '加糖和牛奶' );
};
CoffeeWithHook.prototype.customerWantsCondiments = function(){
return window.confirm( '請(qǐng)問需要調(diào)料嗎?' );
};
var coffeeWithHook = new CoffeeWithHook();
coffeeWithHook.init();
好萊塢原則
學(xué)習(xí)完模板方法模式之后,我們要引入一個(gè)新的設(shè)計(jì)原則——著名的“好萊塢原則”。
好萊塢無疑是演員的天堂,但好萊塢也有很多找不到工作的新人演員,許多新人演員在好萊塢把簡(jiǎn)歷遞給演藝公司之后就只有回家等待電話。有時(shí)候該演員等得不耐煩了,給演藝公司打電話詢問情況,演藝公司往往這樣回答:“不要來找我,我會(huì)給你打電話?!?/p>
在設(shè)計(jì)中,這樣的規(guī)則就稱為好萊塢原則。在這一原則的指導(dǎo)下,我們?cè)试S底層組件將自己掛鉤到高層組件中,而高層組件會(huì)決定什么時(shí)候、以何種方式去使用這些底層組件,高層組件對(duì)待底層組件的方式,跟演藝公司對(duì)待新人演員一樣,都是“別調(diào)用我們,我們會(huì)調(diào)用你”。
模板方法模式是好萊塢原則的一個(gè)典型使用場(chǎng)景,它與好萊塢原則的聯(lián)系非常明顯,當(dāng)我們用模板方法模式編寫一個(gè)程序時(shí),就意味著子類放棄了對(duì)自己的控制權(quán),而是改為父類通知子類,哪些方法應(yīng)該在什么時(shí)候被調(diào)用。作為子類,只負(fù)責(zé)提供一些設(shè)計(jì)上的細(xì)節(jié)。
除此之外,好萊塢原則還常常應(yīng)用于其他模式和場(chǎng)景,例如發(fā)布-訂閱模式和回調(diào)函數(shù)。
- 發(fā)布—訂閱模式
在發(fā)布—訂閱模式中,發(fā)布者會(huì)把消息推送給訂閱者,這取代了原先不斷去fetch消息的形式。例如假設(shè)我們乘坐出租車去一個(gè)不了解的地方,除了每過5秒鐘就問司機(jī)“是否到達(dá)目的地”之外,還可以在車上美美地睡上一覺,然后跟司機(jī)說好,等目的地到了就叫醒你。這也相當(dāng)于好萊塢原則中提到的“別調(diào)用我們,我們會(huì)調(diào)用你”。
- 回調(diào)函數(shù)
在ajax異步請(qǐng)求中,由于不知道請(qǐng)求返回的具體時(shí)間,而通過輪詢?nèi)ヅ袛嗍欠穹祷財(cái)?shù)據(jù),這顯然是不理智的行為。所以我們通常會(huì)把接下來的操作放在回調(diào)函數(shù)中,傳入發(fā)起ajax異步請(qǐng)求的函數(shù)。當(dāng)數(shù)據(jù)返回之后,這個(gè)回調(diào)函數(shù)才被執(zhí)行,這也是好萊塢原則的一種體現(xiàn)。把需要執(zhí)行的操作封裝在回調(diào)函數(shù)里,然后把主動(dòng)權(quán)交給另外一個(gè)函數(shù)。至于回調(diào)函數(shù)什么時(shí)候被執(zhí)行,則是另外一個(gè)函數(shù)控制的。
真的需要“繼承”嗎
模板方法模式是基于繼承的一種設(shè)計(jì)模式,父類封裝了子類的算法框架和方法的執(zhí)行順序,子類繼承父類之后,父類通知子類執(zhí)行這些方法,好萊塢原則很好地詮釋了這種設(shè)計(jì)技巧,即高層組件調(diào)用底層組件。
本文我們通過模板方法模式,編寫了一個(gè)Coffee or Tea的例子。模板方法模式是為數(shù)不多的基于繼承的設(shè)計(jì)模式,但JavaScript語言實(shí)際上沒有提供真正的類式繼承,繼承是通過對(duì)象與對(duì)象之間的委托來實(shí)現(xiàn)的。也就是說,雖然我們?cè)谛问缴辖梃b了提供類式繼承的語言,但這里學(xué)習(xí)到的模板方法模式并不十分正宗。而且在JavaScript這般靈活的語言中,實(shí)現(xiàn)這樣一個(gè)例子,是否真的需要繼承這種重武器呢?
在好萊塢原則的指導(dǎo)之下,下面這段代碼可以達(dá)到和繼承一樣的效果。
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( '把咖啡倒進(jìn)杯子' );
},
addCondiments: function(){
console.log( '加糖和牛奶' );
}
});
var Tea = Beverage({
brew: function(){
console.log( '用沸水浸泡茶葉' );
},
pourInCup: function(){
console.log( '把茶倒進(jìn)杯子' );
},
addCondiments: function(){
console.log( '加檸檬' );
}
});
var coffee = new Coffee();
coffee.init();
var tea = new Tea();
tea.init();
在這段代碼中,我們把brew、pourInCup、addCondiments這些方法依次傳入Beverage函數(shù),Beverage 函數(shù)被調(diào)用之后返回構(gòu)造器F。F類中包含了“模板方法”F.prototype.init。跟繼承得到的效果一樣,該“模板方法”里依然封裝了飲料子類的算法框架。
小結(jié)
模板方法模式是一種典型的通過封裝變化提高系統(tǒng)擴(kuò)展性的設(shè)計(jì)模式。在傳統(tǒng)的面向?qū)ο笳Z言中,一個(gè)運(yùn)用了模板方法模式的程序中,子類的方法種類和執(zhí)行順序都是不變的,所以我們把這部分邏輯抽象到父類的模板方法里面。而子類的方法具體怎么實(shí)現(xiàn)則是可變的,于是我們把這部分變化的邏輯封裝到子類中。通過增加新的子類,我們便能給系統(tǒng)增加新的功能,并不需要改動(dòng)抽象父類以及其他子類,這也是符合開放-封閉原則的。
但在JavaScript中,我們很多時(shí)候都不需要依樣畫瓢地去實(shí)現(xiàn)一個(gè)模版方法模式,高階函數(shù)是更好的選擇。