[Design Patterns-Python]三.樣板方法模式

Daniel Chiang
Feb 1, 2021

--

情境

你有吃過吃到飽嗎?應該很多人在吃到飽店裡都看過一種熱飲飲料機,可以製作咖啡、熱可可、熱奶茶等。今天你被指派做這台飲料機。操作相當簡單,使用者只要點選指定飲料後,按下準備按鈕,飲料就會從管口流出。
身為開發者的你要知道以下幾種飲料在機器內部製作過程:

  • Hot Coco:
  1. 倒入熱可可粉。
  2. 用熱水浸泡可可粉。
  3. 倒入杯中。
  • Coffee:
  1. 倒入濃縮咖啡粉。
  2. 用熱水浸泡咖啡。
  3. 倒入杯中。
  4. 加糖。

實做

在開始前想想你會怎麼做?
為了示範,我們先來看看比較不聰明的作法。

  1. 建造兩個飲料類別:Coco、Coffee。
  2. 各自類別實做製作過程的方法以及prepare方法。

設計模式實作

我想你也已經看出來上面那段code不夠好,最大的問題在於有太多重複的code,以及兩個飲料實做了幾乎相同的演算法。因此我們可以定義一個高階的元件當作基底類別,並將子類別prepare的方法抽象到基底類別。

實做基底類別

  1. 建立BaseDrink的抽象類別。
  2. add_coco_powder, add_coffee_powder 可以將名字通用化為add_powder,以及add_condiments,飲料各自實踐方式不同,我們宣告他們為抽象方法,由子類別提供實踐方法。
  3. pour_coco_into_cup 與pour_coffee_into_cup可以將名字通用化為pour_into_cup,以及brew,飲料都有著一樣的實踐方式。將這些相同實踐行為的方法由基底類別實踐。
  4. 宣告prepare方法,定義了演算法步驟。

實做次類別

  1. 繼承BaseDrink。
  2. 提供抽類方法實做細節。

操作飲料機

  1. 建立operate.py。
  2. 宣告一個 DrinkMachine 類別,定義order方法指定飲料;定義press_prepare_button方法透過指定飲料call prepare。

BaseDrink基底類別的結構已經包含了樣板方法,現在我們來看看什麼是樣板方法模式。BaseDrink類別中的prepare方法就是我們的樣板方法。因為在prepare中定義了演算法步驟,可以把他看成演算法的樣板,並且它允許次類別提供實踐方式。
我們可以追蹤樣板是怎麼運作的。回到上面operate.py main的例子,我們操作DrinkMachine點了一杯coco後,按下準備按鈕呼叫了樣板方法,執行過程就依循規定的演算法步驟。先倒入可可粉(這件事只有次類別知道怎麼做)->用熱水浸泡可可粉->把可可倒入杯中->不加配料。

定義

樣板方法模式(Template Method Pattern):將一個演算法的骨架定義在一個方法中,而演算法本身會用到一些方法,則是定義在次類別中。樣板方法讓次類別在不改變演算法架構的情況下,重新定義演算法中的某些步驟。

我們再看看模式為我們帶來哪些好處。

  1. 過去由coffee, coco控制著演算法,現在由BaseDrink控制、保護演算法,現在演算法只在一個地方,所以容易修改。
  2. 移除飲料類別之間重複的程式碼,BaseDrink提高了程式碼再用性(reuse)。
  3. 樣板方法提供的是一個框架,整合了所有飲料,如果要加新的飲料只需要實踐自己的方法就可以,而過去的做法須要費很多工。
  4. BaseDrink讓我們更容易理解演算法,由次類別負責提供完整的實踐細節,而過去的做法,演算法知識、實踐方式散落在各個類別中。

掛鉤(hook)

想不到還沒結束吧。
我們在抽象類別中定義一個什麼都不做的方法,它的目的是讓次類別能夠對演算法進行掛鉤,次類別可以視情況決定要不要利用推翻(override),提供掛鉤的內容。
因此我們可以這麼做:

  1. 在BaseDrink類別中新增一掛鉤want_condiments並return True。
  2. 改寫prepare方法,新增一個判斷式,判斷want_condiments為True就執行add_condiments。
  3. 次類別自己決定要不要推翻want_condiments。

在次執行operate.py就會發現,點coco的時候不會再出現add nothing因為我們推翻了want_condiments;點coffee的時候會詢問你要不要加糖。

掛鉤的使用時機: 當你的次類別必須必須必須提供演算法某個步驟的實踐時,就使用抽象方法 。但是!如果這個演算法的這個部分是選擇性的,就使用掛鉤。
我們應該讓抽象方法越少越好,這樣次類別在實踐時就越輕鬆。因此如果有些步驟是選擇性的,我們就可以使用掛勾,而不是使用抽象方法增加次類別的負擔。

如果你了解了,應該就會知道其實可以把add_condiments設成hook,因為對於coco或是coffee都是選擇性的功能,就不需要want_condiments。

在你學會了樣板方法也學會了新的守則:

好萊塢守則: 別呼叫我們,我們會呼叫你。

不知道有沒有人跟我一樣覺得為什麼守則的定義看起來都這麼曖昧不清。
我們先從我們的例子來看,BaseDrink是我們的高階元件,它控制了演算法,只有在有需要的時候才會呼叫次類別實踐某個步驟,我們的次類別(Coco, Coffee)如果沒有先被呼叫,是絕對不會直接呼叫抽象類別,他們僅提供了實踐細節。
好萊塢守則可以防止依賴腐敗,依賴腐敗就是高階元件依賴低階元件,低階元件也依賴高階元件,高階元件又依賴邊側元件(星爺:你他媽搞得我好亂啊)
在好萊塢守則,高階元件會決定何時用到低階元件,並決定如何使用它。並且可以使用掛鉤的技巧,允許低階元件掛鉤進來,同時又讓高階的元件不會太依賴這些低階的元件。

比較樣板方法模式與策略模式

樣板方法模式的目的在定義一個"演算法大綱",利用繼承的方式對演算法控制,由次類別定義部分步驟踐細節。而策略模式定義了"演算法家族",並透過物件『合成』的方式,讓使用者選擇不同的演算法實踐方式,但沒辦法控制使用者。(可以多注意粗體字所表示的差異)
樣板方法模式使用了繼承,減少了重複的程式碼,以及較少的物件讓程式沒那麼複雜;策略模式使用了合成,它不依賴任何人,但樣板方法模式依賴超類別的方法實踐,因此策略模式有更好的彈性,讓使用者在執行期能動態地改變策略。

總結

樣板方法模式是一種很常見的模式,尤其是設計框架。但現實中情況一定比範例更加複雜,再繼續往後會學到更多的模式,但最難學到的還是從你程式中找出模式,並且識別出模式的變形。

本篇範例程式

下一篇工廠模式

--

--