# 範本方法模式(Template Method)

範本方法模式的定義是:<br />
> _一種只需要繼承就可以實作的非常簡單的模式,由兩個部份結構組成,第一部分是抽象父類別,第二部分是具體的實作子類別。_
假設我們有兩個平行的子類別,各子類別分別有一些相同的行為,為了避免重複,可以將相同的行為的實作搬到父類別,不同的行為則由子類別實作,很好地體現泛化的思想。
### Coffee or Tea

這邊直接引用書上的例子,實際感受一下這兩個類別的異同之處。
下面是一個咖啡類別 `Coffee`:
```javascript
class Coffee {
boilWater() {
console.log('把水煮沸')
}
brewCoffeeGriends() {
console.log('用沸水沖泡咖啡')
}
pourInCup() {
console.log('把咖啡倒進杯子')
}
addSugarAndMilk() {
console.log('加糖和牛奶')
}
init() {
this.boilWater()
this.brewCoffeeGriends()
this.pourInCup()
this.addSugarAndMilk()
}
}
const coffee = new Coffee()
coffee.init()
```
下面是一個茶類別 `Tea`:
```javascript
class Tea {
boilWater() {
console.log('把水煮沸')
}
steepTeaBag() {
console.log('用沸水浸泡茶葉')
}
pourInCup() {
console.log('把茶水倒進杯子')
}
addLemon() {
console.log('加檸檬')
}
init() {
this.boilWater()
this.steepTeaBag()
this.pourInCup()
this.addLemon()
}
}
const tea = new Tea()
tea.init()
```
嗯...有在寫程式的工程師們應該已經開始頭痛了,這兩個類別相似的點也太多了吧,先冷靜分析一下兩者的異同-除了把 `boilWater` 相同以外,其他三個方法可以被抽象化成意近的函式,並由子類別實作。
開始以範本方法重構吧~<br />
首先,定義抽象父類別 `Beverage` 並將相同的方法統一放置:
```javascript
class Beverage {
boilWater() {
console.log('把水煮沸')
}
// brewCoffeeGriends 和 steepTeaBag 被抽象成「泡」的動作
brew() {}
// pourInCup 取得不錯不用改 不過泡法不同依然要由子類別實作
pourInCup() {}
// addSugarAndMilk 和 addLemon 被抽象成「加調味料」的動作
addCondiments() {}
init() {
this.boilWater()
this.brew()
this.pourInCup()
this.addCondiments()
}
}
```
接著,設計具體實作的子類別,<br />
以下是重構後的咖啡類別 `Coffee`:
```javascript
class Coffee extends Beverage {
brew() {
console.log('用沸水沖泡咖啡')
}
pourInCup() {
console.log('把咖啡倒進杯子')
}
addCondiments() {
console.log('加糖和牛奶')
}
}
const coffee = new Coffee()
coffee.init()
```
以下是重構後的茶類別 `Tea`:
```javascript
class Tea extends Beverage {
brew() {
console.log('用沸水浸泡茶葉')
}
pourInCup() {
console.log('把茶水倒進杯子')
}
addCondiments() {
console.log('加檸檬')
}
}
const tea = new Tea()
tea.init()
```
如此改動不只讓程式顯得一致,也利用原型鏈將相同的方法繼承避免重複。那麼,這個模式所指的範本方法到底是誰呢?在上面這個例子中,答案是 `Beverage.prototype.init`,它作為演算法的腳本,**指導子類別用何種順序去執行哪些方法**。
### 使用場景
該模式經常被架構師用於搭建專案的框架,架構師定好框架的骨架,程式設計師繼承框架的結構之後,負責往裡面填空。
### 真的需要繼承嗎?
範本方法是為數不多的基於繼承的設計模式,但 JavaScript 實際上沒有提供真正的類別式繼承,不過憑藉[好萊塢原則](https://baike.baidu.com/item/%E5%A5%BD%E8%8E%B1%E5%9D%9E%E5%8E%9F%E5%88%99/16019700)-**高層元件呼叫底層元件**的心法,我們也可以通過**物件和物件之間的委託**來實作,形式上地借鑿了提供類別式的語言。
```javascript
const Beverage = function (params) {
const boilWater = function () {
console.log('把水煮沸')
}
// 若一定要由子物件實作可以在預設 function 動手腳防呆
const brew =
params.brew ||
function () {
throw new Error('必須傳遞 brew 方法')
}
const pourInCup =
params.pourInCup ||
function () {
throw new Error('必須傳遞 pourInCup 方法')
}
const addCondiments =
params.addCondiments ||
function () {
throw new Error('必須傳遞 addCondiments 方法')
}
const F = function () {}
// 注意,箭頭函式沒有 prototype,請乖乖使用 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()
```
###### tags: `JavaScript 設計模式與開發實踐` `設計模式` `JavaScript`