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