你有一家飲料店,主要提供咖啡跟茶。泡咖啡跟泡茶的程式碼如以下所示:
public class Coffee { // 這是店裡統一的泡咖啡SOP void prepareRecipe() { // 煮開水 boilWater(); // 用沸水沖泡咖啡 brewCoffeeGrinds(); // 把咖啡倒進杯子 pourInCup(); // 加糖和牛奶 addSugarAndMilk(); } // 底下省略這些方法的實作... }
public class Tea { // 這是店裡統一的泡茶SOP void prepareRecipe() { // 煮開水 boilWater(); // 用沸水沖泡茶包 steepTeaBag(); // 把茶倒進杯子 pourInCup(); // 加檸檬 addSLemon(); } // 底下省略這些方法的實作... }prepareRecipe看起來好像喔,應該可以抽象化。第一步很自然的把一些一樣的方法,如biolWater 跟 pourInCup,放到超類別裡,就稱為 Beverage。而雖然咖啡是沖泡「咖啡」、加糖跟牛奶,茶是沖泡「茶葉」、加檸檬,兩種飲料加的東西不一樣,但是做的「動作」是一樣的,只是處裡不同的原料而已。有了這些想法後,就可以來改上面的程式碼了:
public abstract class Beverage { // 宣告為 final 是因為不想次類別 // 推翻這個方法, 這是統一的演算法 final void prepareRecipe() { // 煮開水 boilWater(); // 用沸水沖泡 // 跟上面程式碼比, 方法名更通用 brew(); // 把飲料倒進杯子 pourInCup(); // 加配料 // 跟上面程式碼比, 方法名更通用 addCondiments(); } // 因為咖啡跟茶處理這些方法的做法不同, // 所以宣告為抽象方法, // 留給次類別去處理 abstract void brew(); abstract void addCondiments(); private void boilWater() { // 不管是茶或咖啡做法都一樣 // 可以直接把實作寫在超類別 } private void pourInCup() { // 不管是茶或咖啡做法都一樣 // 可以直接把實作寫在超類別 } }
public class Tea extends Beverage { @Override public void brew() { System.out.println("Steeping the tea"); } @Override public void addCondiments() { System.out.println("Adding lemon"); } } public class Coffee extends Beverage { @Override public void brew() { System.out.println("Dripping coffee through filter"); } @Override public void addCondiments() { System.out.println("Adding sugar and milk"); } }基本上改寫過後的程式碼,已經是「樣板方法模式」的實踐了。由修改過後的程式碼可以看到,把要做的事情(演算法)包裝起來,就是 prepareRecipe ,而內部實際上又被分為很多子方法。有些方法是共同的,可以定義在超類別,而其它依次類別不同而有不同處理的方法,就留給次類別去實踐。這些就是樣板方法模式的精神。了解此模式後,就來看看正式的定義跟類別圖吧:
樣板方法模式將一個演算法的骨架定義在一個方法中,而演算法本身會用到的一些方法,則是定義在次類別中。樣板方法讓次類別在不改變演算法架構的情況下,重新定義演算法中的某些步驟。
Define the skeleton of an algorithm in an operation, deferring some steps to sub classes. Template Method lets sub classes redefine certain steps of an algorithm without changing the algorithm's structure.
Define the skeleton of an algorithm in an operation, deferring some steps to sub classes. Template Method lets sub classes redefine certain steps of an algorithm without changing the algorithm's structure.
由上述可知道,此模式是用來建立一個演算法的樣板。更具體一點說,是把演算法分成多組步驟。其中任何步驟都可以是抽象方法,由次類別負責實作這些抽象方法。這可以確保演算法的結構維持不變,同時由次類別提供部份實作方式。
樣板方法除了上面的範例外,還有一個小技巧可以用,稱為掛鉤(Hook)。掛鉤是一種方法,被宣告在抽象類別中,且定義為什麼都不做,或是有預設的實作方式。這可以讓次類別有能力對演算法進行掛鉤。要不要掛鉤則由次類別決定。我們來把上面的範例加上掛鉤看看吧:
public abstract class BeverageWithHook { final void prepareRecipe() { boilWater(); brew(); pourInCup(); // 加上一個判斷式, 如果客戶 // 想要配料才真的加配料 if(customerWantsCondiments()) { addCondiments(); } } abstract void brew(); abstract void addCondiments(); private void boilWater() { // 不管是茶或咖啡做法都一樣 // 可以直接把實作寫在超類別 } private void pourInCup() { // 不管是茶或咖啡做法都一樣 // 可以直接把實作寫在超類別 } // 這就是一個掛鉤, 通常是空的實作。 // 次類別可以推翻(Override) 它, // 但不見得要這麼做 public boolean customerWantsCondiments() { return true } }假如要使用掛鉤,次類別要推翻預設的定義,此範例的掛鉤是控制是否要加配料,我們可以在次類別決要怎樣的情形才會加配料:
public class CoffeeWithHook extends BeverageWithHook { @Override public void brew() { System.out.println("Dripping coffee through filter"); } @Override public void addCondiments() { System.out.println("Adding sugar and milk"); } @Override public boolean customerWantsCondiments() { // 推翻掛鉤方法, 改成自己想要的行為 String answer = getUserInput(); if(answer.toLowerCase().startWith("y")) { return true; } return false; } private String getUserInput() { // 省略從標準 I/O 取得使用者輸入... } }接下來可以介紹一個新設計守則,「好萊塢守則」:別呼叫 (打電話給) 我們,我們會呼叫 (打電話給) 你。此守則可以給我們一個方式防止「依賴腐敗」。意思就是當高階元件依賴低階元件,而低階元件又依賴高階元件時,沒有人可以輕易搞懂系統是如何設計的。
在好萊塢守則之下,允許低階元件自己掛鉤在系統上,但是由高階元件決定何時使用低階元件。高階元件對待低階元件的方式就是「別呼叫我們,我們會呼叫你」。由上面的例子來看,樣板方法模式是有符合這個守則的,而之前提過的工廠方法模式以及觀察者模式也都有符合好萊塢守則喔!而工廠方法模式又是樣板方法模式的一種特殊版本,對工廠方法陌生的人可以再去複習一下。
有人可能會對好萊塢守則跟顛覆依賴守則有所混淆。顛覆依賴守則主要是在強調如何在設計中避免相依性,而好萊塢守則是教我們一個技巧,建立有彈性的設計,能讓低階元件掛鉤進來,又不會讓高階元件太依賴這些低階元件。而好萊塢守則也沒有說低階元件不能呼叫高階元件的方法,在實務上常常會在方法內用 super.xxx 來呼叫超類別的方法。
參考資料:
深入淺出設計模式(Head First Design Patterns)
深入淺出設計模式(Head First Design Patterns)
留言
張貼留言