1 模板方法模式定義
模板方法模式:只需要使用整合就能實現。由兩部分組成:抽象父類 + 具體的實現子類。
- 抽象父類:封裝子類的演算法框架,包括實現一些公用方法以及封裝在子類中所有方法的執行順序
- 實現子類:通過整合這個抽象類,也繼承了整個演算法,並且可以選擇重寫父類的方法 假如我們有許多平行的類,各個類之間有許多相同的行為,也有部分不同的行為。如果各位都定義自己所有的行為,那麼會出現很多重複的方法。此時可以將相同的行為搬移到另外一個單一的地方,模板方法模式就是為了解決這個問題。在模板方法模式中,子類中相同的行為被移動到了父類中,而將不同的部分留待子類來實現。
2 咖啡與茶(模板方法模式案例)
我們現在需要泡一杯茶和咖啡,思考泡茶與咖啡的過程。
不管泡茶還是咖啡都會有四個步驟,總結出來如下。我們抽象一個父類表示泡一杯飲料的過程。
- 煮沸水,相同點
- 沸水+原料(不同點,咖啡,茶葉)
- 將飲料倒入杯子,相同點
- 加調料(不同點:糖與牛奶,檸檬)
抽象父類
由於javascript沒有型別檢查,我們需要讓子類必須實現brew, pourInup和addCondiments,因此這裡通過丟擲異常來提醒編寫者。
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();
}
複製程式碼
建立Coffee子類
接下來要重寫抽象父類中的一些方法。只要把水煮沸
這個行為可以直接使用父類。
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( '加糖和牛奶' );
};
// 當呼叫init方法時,會找到父類的init方法進行呼叫。
var coffee = new Coffee();
coffee.init();
複製程式碼
建立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();
複製程式碼
模板方法
上面的例子,tea和Coffee都繼承了Beverage,那麼誰是模板方法呢?Beverage.prototype.init
就是模板方法。因為它內部封裝了子類的演算法框架,它作為一個演算法的模板,知道子類以何種順序執行哪些方法。
3 鉤子方法
平時遇到正常的,喝咖啡的都是上面的順序,但是如果有些人不喜歡加調料,那麼上面的步驟又不符合情況了,此時可以通過鉤子函式來進行解決。鉤子函式通過使用者返回的結果來決定接下來的步驟。究竟要不要鉤子由子類自己決定。
var Beverage = function() { };
Beverage.prototype.boilWater = function() {
console.log('煮沸水');
};
Beverage.prototype.brew = function(){
throw new Error( '子類必須重寫 brew 方法' );
};
....
// 鉤子函式
Beverage.prototype.customerWantsCondiments = function(){
return true;
};
Beverage.prototype.init = function() {
this.boilWater();
this.brew();
this.pourInCup();
// 如果鉤子函式返回true,則新增調料
if (this,customerWantsCondiments()) {
this.addCondiments();
}
}
複製程式碼
在子類Coffee中,需要實現鉤子函式
var Coffee = function() { };
Coffee.prototype = new Beverage();
Coffee.prototype.brew = function(){
console.log( '用沸水沖泡咖啡' );
};
...
// 重寫鉤子函式
Coffee.prototype.customerWantsCondiments = function(){
return window.confirm('請問需要調料嗎?');
};
// 當呼叫init方法時,會找到父類的init方法進行呼叫。
var coffee = new Coffee();
coffee.init();
複製程式碼
4 真的需要繼承嗎
模板方法模式就是基於繼承的一種設計模式,父類中封裝了子類的演算法框架和執行順序,子類繼承父類後,父類通知子類執行這些方法。但是javascript並沒有提供真正的類式繼承,繼承是通過物件與物件之間的委託來實現的,也就是形式上借鑑了提供類式的語言。下面這段程式碼能夠達到一樣的繼承效果。
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( '把咖啡倒進杯子' );
},
addCondiments: function(){
console.log( '加糖和牛奶' );
}
});
複製程式碼
5 小結
模板方法模式在傳統的程式語言中,子類的方法種類以及執行順序都是不變的,這部分邏輯我們都抽象到了父類中,而子類的方法具體怎麼實現是可變的,通過重寫父類的方法,將變化的邏輯部分封裝到子類中。通過增加新的子類,我們能夠給系統增加新的功能的,俺是並不需要修改父類以及其他的子類,這也符合開放-封閉
原則。在javascript中,我們不需要依樣畫瓢去實現一個模板方法模式,因為高階函式是一個更好的選擇。