今天有一家家電廠商,想請你設計一個控制家電自動化的遙控器。遙控器上有多個插槽可控制不同家電,每個家電在遙控器上要提供開、關按鈕。廠商已提供控制家電的 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)

留言
張貼留言