跳到主要內容

樣板方法模式 (Template Method Pattern)

        你有一家飲料店,主要提供咖啡跟茶。泡咖啡跟泡茶的程式碼如以下所示:
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.


        由上述可知道,此模式是用來建立一個演算法的樣板。更具體一點說,是把演算法分成多組步驟。其中任何步驟都可以是抽象方法,由次類別負責實作這些抽象方法。這可以確保演算法的結構維持不變,同時由次類別提供部份實作方式。

        樣板方法除了上面的範例外,還有一個小技巧可以用,稱為掛鉤(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)

留言

這個網誌中的熱門文章

整理設計模式

        依據 GOF 的書,可以將經典的設計模式分為以下三類:生成、行為、結構。 生成模式 :牽涉到 將物件實體化 。這類模式都提供一個方法,將客戶從所需要實體化的物件中鬆綁出來。 獨體模式 (Singleton Pattern) 工廠方法模式 (Factory Method Pattern) 抽象工廠模式 (Abstract Factory Pattern) 建立者模式 (Builder Pattern) 原型模式 (Prototype Pattern) 結構模式 :讓你 合成類別或物件到大型的結構 。 裝飾者模式 (Decorator Pattern) 轉接器模式 (Adapter Pattern) 表象模式 (Facade Pattern) 合成模式 (Composite Pattern) 代理人模式 (Proxy Pattern) 橋接模式 (Bridge Pattern) 享元模式 (Flyweight Pattern) 行為模式 :模述 類別和物件如何互動 ,以及 各自的責任 。 策略模式 (Strategy Pattern) 觀察者模式 (Observer Pattern) 命令模式 (Command Pattern) 樣板方法模式 (Template Method Pattern) 反覆器模式 (Iterator Pattern) 狀態模式 (State Pattern) 責任鏈模式 (Chain of Responsibility Pattern) 解譯器模式 (Interpreter Pattern) 中介者模式 (Mediator Pattern) 備忘錄模式 (Memento Pattern) 訪問者模式 (Visitor Pattern)         有人可能會覺得裝飾者模式明明有替物件增加行為,為什麼不算是行為模式呢?我們可以從上面的結構模式得知, 結構模式用來描述類別或物件如何被合成,以建立新的結構或功能 。裝飾者模式允許你透過「 將某物件包裝進另一個物件的方式 」,將物件合成以提供新功能,因此焦點應該放在「 動態合成物件,以取得某功能 」,而不是物件之間的溝通。         設入淺出設計模式也有提到一些使用設計模式的

訪問者模式 (Visitor Pattern)

        假設你設計一個系統,其中會有一些相似類別,類別中都有某些方法內容相似,但還是需要判斷目前要做事的是哪個類別才能呼叫對應的適當類別。通常遇到這種情情,在 Java 中最直接的做法就是使用 instanceof 關鍵字來判斷,如以下的簡單範例: public interface CarComponent { public void printMessage(); } public class Wheel implements CarComponent { @Override public void printMessage() { System.out.println("This is a wheel"); } // 這是 Wheel 跟 Engine 不同的方法 public void doWheel() { System.out.println("Checking wheel..."); } } public class Engine implements CarComponent { @Override public void printMessage() { System.out.println("This is a engine"); } // 這是 Wheel 跟 Engine 不同的方法 public void doEngine() { System.out.println("Testing this engine..."); } } public class Car { private List mComponents; public Car() { mComponents = new ArrayList<carcomponent>(); } // 有些時候我們還是需要針對不同類別去做不同的事情 public void setComponent(CarCompon

裝飾者模式 (Decorator Pattern)

        假如你有一間飲料店, 目前只有賣幾種咖啡。因為生意很好, 因此想更換菜單…         以下是目前菜單的類別圖:         簡單說明此類別圖, cost() 是抽象的, 子類別要實作自己的 cost() 來告知飲料的價格。         買咖啡時, 也可要求要加料, 例如牛奶(Milk)、摩卡(Mocha,就是巧克力口味)。這樣的新類別要如何設計呢 ? 看起來是不能直接新增所需的子類別, 例如 EspressoWithMilk, EspressoWithMilkAndMocha, DarkRoastWithMilk, DarkRoastWithMilkAndMocha… 這樣加下去, 日後飲料跟配料越來越多時, 類別也就越多, 這實在不是個好設計。         換個方式設計呢, 在 Beverage 裡面加入所有的配料如何 ? 這樣好像也不太好, 未來要是配料有更動, Beverage 程式碼就要重寫, 而未來要是有新口味的飲料時, 有些配料就不太合理 ( 薑茶加摩卡 ? ), 更麻煩的是, 無法應付機車的客人 (例如要加 3 份牛奶)。這時候裝飾者模式就能上場啦。在介紹裝飾者模式前, 先說明其設計守則: 類別應該開放, 以便擴充 ; 應該關閉, 禁止修改。         我們的目標是允許類別容易擴充, 在不修改現有程式碼的情形就能搭配新的行為。這樣的設計具有彈性, 可以接受新功能以達到改變需求的目的。這看起來好像有點矛盾, 但是的確有一些技術可以在不直接修改程式碼的情形下進行擴充, 如裝飾者模式。         這時候應該有人會問: 那是不是以後我的專案架構設計都遵循這個守則就是好設計了 ? 答案是不太可能, 也沒這必要, 就算做得到, 也可能是浪費, 容易導致程式碼複雜且難以理解。只需小心選擇哪些部分未來會擴充, 這些部份遵循這個設計守則即可。         接下來正式介紹裝飾者模式的定義:  裝飾者模式動態地將責任加諸於物件上。若要擴充功能,裝飾者模提供了比繼承更有彈性的選擇 Attach additional responsibilities to an object dynamically. Decorators provide a flexible alt