跳到主要內容

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

解譯器模式 (Interpreter Pattern)

        解譯器模式簡單來說就是 把一句有特殊規則的語句,透過解釋器將它真正的意思表現出來 。相信有學過 Context-free grammar (CFG),Backus–Naur form (BNF),或是 Compiler 相關程的人會比較了解這個模式 (不是我,我早就忘光了…)。不知道上面術語的人,還是可以透過接下來的介紹來稍微了解這個模式。先來看一下這個模式的正式定義及類別圖: 定義一個語言與其文法,使用一個解譯器來表示這個語言的敘述 Given a language, define a representation for its grammar along with an interpreter that uses the representation to interpret sentences in the language Context 通常是指待解譯的語句 AbstractExpression 是所有規則都要實作的介面 TerminalExpression 是指無法再展開的規則,算是最小單位的規則 NonterminalExpression 是指可以再展開的規則,可以展開成 NonterninalExpression 和 TerminalExpression 的組合         從類別圖可以看到,語法可能可以一直展開,這時就可以用語法樹來表示,而語法樹以程式來表達的話,就可以使用 合成模式 。而通常比較正式的語法會用 BNF 來表示,如下: expression ::= plus | minus | variable | number plus ::= expression expression '+' minus ::= expression expression '-' variable ::= 'a' | 'b' | 'c' | ... | 'z' digit = '0' | '1' | ... | '9' number ::= digit | digit number 而 每個語法都會定義一個類別 ,如 pl...

代理人模式 (Proxy Pattern)

        代理人模式如同字面上的意思,就是做事情時(如取得某些資料),是透過代理人,而不是直接跟提供資料的物件構通。先來看看此模式的正式定義及類別圖: 代理人模式讓某個物件具有一個替身,藉以控制外界對此物件的影響。 Provide a surrogate or placeholder for another object to control access to it.         從類別圖可以看到, Subject 是共用的介面,可以讓客戶 將 Proxy 物件視為 RealSubject 物件 來處理。RealSubject 是真正做事的物件,被 Proxy 代理, Proxy 可以控制 RealSubject 的存取 。Proxy 持有 RealSubject 的參考,客戶和 RealSubject 的互動都要透過 Proxy,在某些情形下這樣的限制是必要的,如 RealSubject 是遠端物件, RealSubject 建立成本高等等。         此模式有許多種變形,都是依據上面的原則而發展出來的。以下介紹  深入淺出設計模式(Head First Design Patterns) 裡面提到的一些應用方式:         1. 遠端代理人(Remote) :算是最常使用到的應用方式。在網路中或是跨 Process 的各種程式,不可能直接存取(assess) 不同程式間的物件,因此就需要遠端代理人。 Java RMI  和 Android AIDL  都算是遠端代理人的實踐。         2. 虛擬代理人(Virtual) :代理的對象是建立很花費資源的物件。當物件建立前和建立中,由虛擬代理人扮演代理的角色。物件建立完成後,代理人就會將讓求直接轉給物件。以下為簡單範例程式: // Proxy 實作跟 RealSubject 共同的介面 Icon public class ImageProxy implements Icon { // 這是我們的 RealSubje...