跳到主要內容

命令模式 (Command Pattern)

         今天有一家家電廠商,想請你設計一個控制家電自動化的遙控器。遙控器上有多個插槽可控制不同家電,每個家電在遙控器上要提供開、關按鈕。廠商已提供控制家電的 API,你只需要設計遙控器 API,可以控制不同家電。注意未來可能會有新的家電。

        看起來好像不好設計。既要能控制現在的家電,還要能滿足擴充性,以便控制新的家電。最重要的是,要怎麼讓「遙控器」和「家電」之間能夠鬆綁呢?有沒有一個設計模式,可以讓「發出需求的物件」「接受與執行需求的物件」分割開呢?這時候就是命令模式派上用場的時候了。使用命令模式,遙控器只要發出需求,如開電燈,遙控器不用知道到底是誰,做了什麼事,遙控器只知道有發出需求。而需求發出後,就是接收者,電燈,去執行需求,如打開電燈。只看文字可能不是很清楚,直接用程式碼來熟悉吧。

        首先我們要先有一個通用的命令介面,以便讓所有家電實做。將來遙控器也是直接對這個通用介面操作即可。
public interface Command {

    // 很簡單的範例,只要一個執行方法
    public void execute();
}
        接下來假設想實踐一個打開電燈的命令,假設廠商提供的電燈 API 裡有 on(),off():
public class LightOnCommand implements Command {

    // 這是廠商提供的,也是實際上處理需求的接收者
    private Light mLight;

    // 傳入實際的電燈讓此命令能控制
    // 一旦呼叫了 execute,就由此電燈物件
    // 成為接收者,負責處理需求
    public LightOnCommand(Light light)
    {
        mLight = light;
    }

    @Override
    public void execute()
    {
        // 接收者處理需求
        // 此例是電燈打開,所以呼叫on()
        mLight.on();
    }
}
        同理我們也可以設計出很多命令,如電燈關,風扇開、關,門開、關等命令。有了這些命令後,就可以接著設計遙控器:
public class RemoteControl {

    // 遙控器要能控制家電的開跟關
    Command[] mOnCommands;
    Command[] mOffCommands;

    public RemoteControl()
    {
        // 假設遙控器能控制7個家電
        mOnCommands = new Command[7];
        mOffCommands = new Command[7];

        Command noCommand = new NoCommand();
        for(int i = 0; i < 7; i++)
        {
            mOnCommands[i] = noCommand;
            mOffCommands[i] = noCommand;
        }
    }

    public void setCommand(int slot,
        Command onCommand, Command offCommand)
    {
        mOnCommands[slot] = onCommand;
        mOffCommands[slot] = offCommand;
    }

    // 當按下開或關的按鈕,就執行對應的命令
    public void onButtonPushed(int slot)
    {
        mOnCommands[slot].execute();
    }

    public void offButtonPushed(int slot)
    {
        mOffCommands[slot].execute();
    }
}
        上面的程式碼中可以看到有 NoCommand,這是空物件 (Null Object) 的範例,因為遙控器不可能一出廠就設定了有意義的命令物件,這時有空物件就很有用。上述提到的類別在使用上可以如以下程式碼所示:
public class RemoteLoader {

    public static void main(String[] args)
    {
        // 建立遙控器物件
        RemoteControl remote = new RemoteControl();

        // 建立實際要操作的家電,也是接收者
        // 這些是廠商提供的
        Light livingRoomLight = new Light("Living Room");
        Door frontDoor = new Door("Front door");

        // 建立遙控器上要執行的命令
        LightOnCommand livingRoomLightOn =
            new LightOnCommand(livingRoomLight);

        LightOffCommand livingRoomLightOff =
            new LightOffCommand(livingRoomLight);

        DoorOpenCommand frontDoorOpen =
            new DoorOpenCommand(frontDoor);

        DoorCloseCommand frontDoorClose =
            new DoorCloseCommand(frontDoor);

        // 把命令加到遙控器裡
        remote.setCommand(0, livingRoomLightOn, livingRoomLightOff);
        remote.setCommand(1, frontDoorOpen, frontDoorClose);

        // 操作家電
        remote.onButtonPushed(0);
        remote.offButtonPushed(0);
        remote.onButtonPushed(1);
        remote.offButtonPushed(1);
    }
}
        最後來看一下命令模式的定義及類別圖吧:

命令模式將「請求」封裝成物件,以便使用不同的請求、佇列、或者日誌,參數化其他物件。命令模式也支援可復原的作業。
Encapsulate a request as an object, thereby letting you give parameter clients with different requests, queue or log requests, and support undoable operations.


  • Client 負責建立一個 ConcreteCommand,並設定其接收者。
  • Invoker 只有一個命令物件,並在某個時機點呼叫命令物件的 execute。
  • ConcreteCommand 同時記錄了動作和接收者,Invoker只要呼叫 execute,就可以發出請求,導致 ConcreteCommand 呼叫接收者的一個或多個動作。
        類別圖裡好像有一個 undo 方法,要怎麼做到回前一個動作呢?以上面的範例來說,LightOnCommand 的 execute 就是把燈打開, 而 undo 就是反過來把燈關掉。這樣想的話就簡單多了。我們的程式可以做以下修改:
public interface Command {

    public void execute();

    // 新加入的方法
    public void undo();
}
public class LightOnCommand implements Command {

    // 這是廠商提供的,也是實際上處理需求的接收者
    private Light mLight;

    // 傳入實際的電燈讓此命令能控制
    // 一旦呼叫了 execute,就由此電燈物件
    // 成為接收者,負責處理需求
    public LightOnCommand(Light light)
    {
        mLight = light;
    }

    @Override
    public void execute()
    {
        // 接收者處理需求
        // 此例是電燈打開,所以呼叫on()
        mLight.on();
    }

    @Override
    public void undo()
    {
        // execute 是打開電燈
        // 因此這邊做的就是關電燈
        mLight.off();
    }
}
        現在還差一步,就是讓遙控器能夠追蹤最後按下去的是什麼鈕:
public class RemoteControl {

    // 遙控器要能控制家電的開跟關
    Command[] mOnCommands;
    Command[] mOffCommands;

    // 多一個變數記錄前一步
    Command mUndoCommand;

    public RemoteControl()
    {
        // 假設遙控器能控制7個家電
        mOnCommands = new Command[7];
        mOffCommands = new Command[7];

        Command noCommand = new NoCommand();
        for(int i = 0; i < 7; i++)
        {
            mOnCommands[i] = noCommand;
            mOffCommands[i] = noCommand;
        }

        // 一開始並沒有前一步
        // 因此設為NoCommand物件
        mUndoCommand = noCommand;
    }

    public void setCommand(int slot,
        Command onCommand, Command offCommand)
    {
        mOnCommands[slot] = onCommand;
        mOffCommands[slot] = offCommand;
    }

    // 當按下開或關的按鈕,就執行對應的命令
    // 並且記錄目前執行了什麼命令
    public void onButtonPushed(int slot)
    {
        mOnCommands[slot].execute();
        mUndoCommand = mOnCommands[slot];
    }

    public void offButtonPushed(int slot)
    {
        mOffCommands[slot].execute();
        mUndoCommand = mOffCommands[slot];
    }

    // 當 undo 按鈕按下去時
    // 只要執行儲存的 command 的 undo
    public void undoButtonPushed()
    {
        mUndoCommand.undo();
    }
}
Command 的 execute 可以做一件事,也可以做很多事,甚至是很多個命令合在一起成為複合式命令:
public class MacroCommand implements Command {

    // 一次記錄多個 Command
    Command[] mCommands;

    public MacroCommand(Command[] commands)
    {
        mCommands = commands;
    }

    @Override
    public void execute()
    {
        for(int i = 0; i < mCommands.length; i++)
        {
            mCommands[i].execute();
        }
    }

    @Override
    public void undo()
    {
        for(int i = 0; i < mCommands.length; i++)
        {
            mCommands[i].undo();
        }
    }
}
        雖然命令模式看起來很強大,但無可避免的,但命令過多時,一樣會產生命令類別太多的情形,因此實務上使用還是加以評估。

參考資料:

        深入淺出設計模式(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