跳到主要內容

策略模式 (Strategy Pattern)

        假設你在公司做了一套鴨子遊戲, 類別圖如下:


        某天, 主管要求你把這遊戲加上新功能: 讓鴨子會飛, 這時程式要怎麼修改呢? 最簡單的方式, 就是在父類別裡加一個 method: fly(), 這樣就所有的鴨子都會飛啦。但是這樣好像怪怪的,因為並不是每種鴨子都會飛。就算把不會飛的鴨子 fly() 裡什麼事都不做, 未來當不會飛的鴨子越來越多, 程式維護起來也很麻煩。

        既然繼承不行,那改成介面(Interface) 應該可以吧 ?  因為不是所有鴨子都會飛跟叫, 因此就把這兩個行為拉出來變 Interface, 其類別圖如下:


        但是這也不是個好的方法。一來程式碼無法再利用, 二來當鴨子的種類變多時, 每種鴨子都要自己實作 Flyable 或 Quackable, 而這時候策略模式就派上用場了。

        定義: 策略模式 定義了演算法家族, 個別封裝起來,  讓它們之間可以互相替換。此模式讓演算法的變動, 不會影響到使用演算法的程式。
Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it. 

        設計守則:
        1. 把程式中可能變動的部份獨立出來
        2. 寫程式是針對介面寫, 不是針對實作方式寫
        3. 多用合成, 少用繼承

        根據守則1, 我們知道目前會變動的就是飛跟叫的行為, 而這兩個行為在上個類別圖已經獨立出來了。根據守則2, 第一版的鴨子程式, 因為功能都加在 Duck 裡, 造成程式沒有彈性, 因此要有其他類別來實作 Flyable 跟 Quackable。更精確一點的說, 是指 Duck 想要有飛或叫的行為, 其變數的宣告應該是抽象類別或介面, 如此可使用多型, 在執行時可以依據不同的實作而執行不同的行為(不懂的話自行複習多型的觀念)。

        由設計守則1 2, 我們可以重新設計這個鴨子遊戲了。當有需要飛的行為時, 就產生飛的類別, 去實作 Flyable, 同理需要叫的行為也是一樣, 其類別圖如下:

        需要用翅膀飛的鴨子就可以用 FlyWithWings, 而不會飛的鴨子就能用 FlyNoWay。


        同理, 會呱呱叫的鴨子就用 Quack, 不會叫的用 MuteQuack。

        這樣的設計, 可以讓飛跟叫的行為被其他鴨子物件再利用, 因為這些行為跟鴨子類別沒關係了。就算我們再新增其他飛或叫的行為,也不會影響到鴨子類別。而修改過後的 Duck class 類別圖如下:
        接下來就是看程式碼了~
public abstract class Duck {

    Flyable flyAble;
    Quackable quackAble;

    public abstract void display();

    public void performFly()
    {
        if(flyAble != null)
            flyAble.fly();
    }

    public void performQuack()
    {
        if(quackAble != null)
            quackAble.quack();
    }

    public void setFlyable(Flyable fly)
    {
        flyAble = fly;
    }

    public void setQuackable(Quackable quack)
    {
        quackAble = quack;
    }
}

假設我們今天有一隻超級鴨, 想要讓牠會超級飛跟超級叫, 程式如下:
public class SuperFly implements Flyable {

    public void fly()
    {
        System.out.println("I'm super flying!!");
    }
}

public class SuperQuack implements Quackable {

    public void quack()
    {
        System.out.println("I'm super quacking!!");
    }
}

public class SuperDuck extends Duck {

    public void display()
    {
        System.out.println("I'm super duck!!");
    }
}

public class DuckTest {

    pubilc static void main(String[] args)
    {
        Duck duck = new SuperDuck();

        // 指定這隻鴨怎麼飛
        duck.setFlyable(new SuperFly());

        // 指定這隻鴨怎麼叫
        duck.setQuackable(new SuperQuack());

        duck.performFly();
        duck.performQuack();
    }
}

        從程式碼可以看出, 未來超級鴨想要有其他不同行為的飛或叫時, 只要新增其類別, 在程式執行期間, 隨時可以更換。
     
        最後看一下加入策略模式後的鴨子類別圖吧:

        從最後的程式碼可以看出, 用合成建立系統有很大的彈性。不但可以把行為(演算法) 封裝成類別, 更可以在執行期間動態改變行為 (演算法), 只要合成的行為物件符合特定的介面即可。這也就是設計守則3 所說的: 多用合成, 少用繼承。




參考資料:

1. 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