今天有一家家電廠商,想請你設計一個控制家電自動化的遙控器。遙控器上有多個插槽可控制不同家電,每個家電在遙控器上要提供開、關按鈕。廠商已提供控制家電的 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.
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 呼叫接收者的一個或多個動作。
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)
深入淺出設計模式(Head First Design Patterns)

留言
張貼留言