深入淺出設計模式(Design Pattern)-樣板方法模式(9-Template Method Pattern)

Ben
生活的各種可能性
8 min readFeb 26, 2024
photo by Rodion Kutsaiev

這篇要來替大家介紹一個應用繼承和抽象來實作共同流程的設計模式,樣板方法模式(Template Method Pattern)。

樣板方法模式(Template Method Pattern)

定義:可在方法裡面定義演算法的骨架,並將一些步驟推遲至子類別處理。樣板方法可讓子類別重新定義演算法的某些步驟,而且不會改變演算法的結構。

接著我們來看看我們這次的情境內容:這次我們要開一家咖啡店,我們除了有賣咖啡之外當然也有賣茶,畢竟咖啡因是現代人重要的精神糧食之一。我們在擬定我們的飲料沖泡流程時發現茶跟咖啡的流程非常相似。

這是我們一開始的做法,我們分別有咖啡和茶兩個class,從下方我們可以看到,它們的流程非常相似(step1和step3是一樣的,但step2和step4則是不一樣的,是它們自己的製作方法)。

class Coffee {
// 沖咖啡的流程
prepareRecipe(): void {

// 每一步都是獨立的方法,step1~step4
this.boilWater();
this.brewCoffeeGrinds();
this.pourInCup();
this.addSugarAndMilk();
}

public boilWater(): void {
// do something...
}

public brewCoffeeGrinds(): void {
// do something
}

public pourInCup(): void {
// do something
}

public addSugarAndMilk(): void {
// do something
}
}

class Tea {
// 沖茶的流程
prepareRecipe(): void {

// 每一步都是獨立的方法,step1~step4
this.boilWater();
this.steepTegBag();
this.pourInCup();
this.addLemon();
}

public boilWater(): void {
// do something...
}

public steepTeaBag(): void {
// do something...
}

public pourInCup(): void {
// do something...
}

public addLemon(): void {
// do something...
}
}

接著我們來把重複的內容抽象化,來看看我們的類別圖

類別關係示意圖

我們新的設計完成了,但總是覺得有些地方怪怪的,是不是有忽略了什麼地方?

我們進一步看看prepareRecipe裡的step2 and step4,它們是的流程非常相似,像是step2的浸泡(steep)和沖泡(brew)其實差異不大,而step4的加入檸檬和加入糖與牛奶都是替飲料加入調味料,差別在於它們是分別處理不同類型的飲料。

有沒有辦法進一步把整個prepareRecipe方法也抽象化呢?

step2 and step4類似示意圖

接下來我們將Coffee and Tea各自的方法也抽象化,現在Coffee和Tea就能共用一個prepareRecipe方法了。

流程抽象示意圖

接著我們來看看完整的範例

// 抽象的caffeine飲料類別
abstract class CaffeineBeverage {
// prepareRecipe定義了一個演算法樣板流程
public prepareRecipe(): void {
this.boilWater();
this.brew();
this.pourInCup();

if (this.customerWantsCondiments()) {
this.addCondiments();
}
}

// 子類別必須實作以下兩個演算法(共同的方法,但實作內容不同,由子類別實現)
abstract brew(): void;
abstract addCondiments(): void;

public boilWater(): void {
console.log("Boiling water");
}

public pourInCup(): void {
console.log("Pouring into cup");
}

// hook: 子類別可以複寫內容來決定是否添加調味
public customerWantsCondiments(): boolean {
return false
}
}

// 實例化茶
class Tea extends CaffeineBeverage {
public brew(): void {
console.log("Steeping the tea");
}

public addCondiments(): void {
console.log("Adding lemon");
}

public customerWantsCondiments(): boolean {
return true
}
}

// 實例化coffee
class Coffee extends CaffeineBeverage {
public brew(): void {
console.log("Dripping Coffee through filter");
}

public addCondiments(): void {
console.log("Adding Sugar and Milk");
}
}

const tea = new Tea();
const coffee = new Coffee();

tea.customerWantsCondiments();
tea.prepareRecipe();
coffee.prepareRecipe();

首先我們會有個抽象的超類別CaffeineBeverage,它定義了prepareRecipe方法, 它是製作Coffee和Tea流程的共同方法,它除了包含boilWater and pourInCup方法之外還擁有兩個方法分別是brew and addCondiments的製作流程方法。

brew and addCondiments這兩個方法在類別裡被定義為抽象方法,我們必須在子類別實作它,這也代表著我們可以分別實現Coffee和Tea各自的內容。

接著我們還定義了一個customerWantsCondiments hook方法可以讓我們的子類別可以決定要不要添加調味料。我們可以覆寫這個方法,或是我們就不理會它。

這樣子我們就完成了能夠擁有共同流程(樣板),還能針對流程中的一些步驟去做客製化實現的目標了。

prepareRecipe這個樣板方法定義了演算法的步驟,可以讓子類別提供一或多個步驟的實作。

接下來要介紹另一條設計原則:

好萊塢原則:別打給我們,我們會打給你。

好萊塢原則可以避免依賴腐敗,意指高階組件依賴一些低階組件,而那些低階組件又依賴一些高階組件,那些高階組件又依賴一些旁系組件,那些旁系組件又依賴低階組件,導致全世界都看不懂整個系統是怎麼設計的。

這個原則的做法是讓低階組件自己掛入系統,由高階組件決定何時使用它們,以及如何使用它們。簡單的說法就是別打給(呼叫)我們,我們會打給(呼叫)你。

接下來我們用這個原則來解釋這個範例的內容

好萊塢原則示意圖

重點提示

  • 樣板方法定義了演算法的步驟,並將這些步驟推遲給子類別實作。
  • 樣板方法模式提供重要的程式碼重複使用技術。
  • 樣板方法的抽象類別可以定義具體方法、抽象方法,以及hook。
  • 抽象方法是由子類別實作的。
  • 掛鉤是在抽象類別裡面不做事或執行預設行為的方法,它可以在子類別裡面覆寫。
  • 好萊塢原則告訴我們,將決策權放在高階組件裡,讓它決定如何與何時呼叫低階模組。
  • 你會在真實世界的程式裡看到許多樣板方法模式,但是(與任何模式一樣)不要期望他們的設計與教科書一樣。
  • 策略與樣板方法都封裝演算法,前者透過組合,後者透過繼承。
  • 工廠方法是樣板方法的專門化版本。

結語

在樣板方法中,使用了繼承和抽象的特性達成了有同樣的樣板流程卻能實作不同流程細節的目標,同時也能避免依賴腐敗。

這種設計模式避免了高低階組件之間混亂的複雜依賴關係,統一的讓高階組件來決定需不需要呼叫低階組件,讓用戶端只依賴抽象的類別,降低對系統的依賴程度。

下一篇要介紹:迭代器模式(Iterator Pattern)

這篇就到這邊瞜,如果有任何問題歡迎留言給我,喜歡的話也別忘了clap起來唷。

--

--