Try   HackMD

範本方法模式(Template Method)

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

範本方法模式的定義是:

一種只需要繼承就可以實作的非常簡單的模式,由兩個部份結構組成,第一部分是抽象父類別,第二部分是具體的實作子類別。

假設我們有兩個平行的子類別,各子類別分別有一些相同的行為,為了避免重複,可以將相同的行為的實作搬到父類別,不同的行為則由子類別實作,很好地體現泛化的思想。

Coffee or Tea

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

這邊直接引用書上的例子,實際感受一下這兩個類別的異同之處。

下面是一個咖啡類別 Coffee

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

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 相同以外,其他三個方法可以被抽象化成意近的函式,並由子類別實作。

開始以範本方法重構吧~
首先,定義抽象父類別 Beverage 並將相同的方法統一放置:

class Beverage {
  boilWater() {
    console.log('把水煮沸')
  }
  // brewCoffeeGriends 和 steepTeaBag 被抽象成「泡」的動作
  brew() {}
  // pourInCup 取得不錯不用改 不過泡法不同依然要由子類別實作
  pourInCup() {}
  // addSugarAndMilk 和 addLemon 被抽象成「加調味料」的動作
  addCondiments() {}
  init() {
    this.boilWater()
    this.brew()
    this.pourInCup()
    this.addCondiments()
  }
}

接著,設計具體實作的子類別,
以下是重構後的咖啡類別 Coffee

class Coffee extends Beverage {
  brew() {
    console.log('用沸水沖泡咖啡')
  }
  pourInCup() {
    console.log('把咖啡倒進杯子')
  }
  addCondiments() {
    console.log('加糖和牛奶')
  }
}

const coffee = new Coffee()
coffee.init()

以下是重構後的茶類別 Tea

class Tea extends Beverage {
  brew() {
    console.log('用沸水浸泡茶葉')
  }
  pourInCup() {
    console.log('把茶水倒進杯子')
  }
  addCondiments() {
    console.log('加檸檬')
  }
}

const tea = new Tea()
tea.init()

如此改動不只讓程式顯得一致,也利用原型鏈將相同的方法繼承避免重複。那麼,這個模式所指的範本方法到底是誰呢?在上面這個例子中,答案是 Beverage.prototype.init,它作為演算法的腳本,指導子類別用何種順序去執行哪些方法

使用場景

該模式經常被架構師用於搭建專案的框架,架構師定好框架的骨架,程式設計師繼承框架的結構之後,負責往裡面填空。

真的需要繼承嗎?

範本方法是為數不多的基於繼承的設計模式,但 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