跳到主要內容

命令模式 (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)

留言

這個網誌中的熱門文章

訪問者模式 (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 份牛奶)。這時候裝飾者模式就能上場啦。在介紹裝飾者模式前, 先說明其設計守則: 類別應該開放, 以便擴充 ; 應該關閉, 禁止修改。         我們的目標是允許類別容易擴充, 在不修改現有程式碼的情形就能搭配新的行為。這樣的設計具有彈性, 可以接受新功能以達到改變需求的目的。這看起來好像有點矛盾, 但是的確有一些技術可以在不直接修改程式碼的情形下進行擴充, 如裝飾者模式。         這時候應該有人會問: 那是不是以後我的專案架構設計都遵循這個守則就是好設計了 ? 答案是不太可能, 也沒這必要, 就算做得到, 也可能是浪費, 容易導致程式碼複雜且難以理解。只需小心選擇哪些部分未來會擴充, 這些部份遵循這個設計守則即可。         接下來...

狀態模式 (State Pattern)

        如果今天你要設計一台如下圖的糖果機,你會怎麼設計呢?         有上過資訊相關課程的人,應該不難從上圖聯想到 狀態圖 ,上圖中每個圓圈都是一個狀態,而每個箭頭就代表狀態的轉換。有了這個概念後,把它轉成程式就不難了: public class CandyMachine { // 以下四個值表示糖果機會用到的狀態 final static int SOLD_OUT = 0; final static int NO_COIN = 1; final static int HAS_COIN = 2; final static int SOLD = 3; // 需要有一個變數來記錄目前的狀態 // 初始設為賣完, 因為一開始機器裡沒糖果 int mState = SOLD_OUT; // 也要有另一個變數記錄目前剩多少顆糖果 int mCount = 0; public CandyMachine(int count) { mCount = count; // 機器內有糖果的話就跳到沒投錢的狀態, // 表示糖果機在等人投錢 if(mCount > 0) { mState = NO_COIN; } } // 當投錢時會執行這個方法 public void insertCoin() { if(mState == HAS_COIN) { System.out.println("You can't insert another coin"); } else if(mState == NO_COIN) { mState == HAS_COIN; System.out.println("You inserted a coin"); } ...