Design Pattern(11) - Command
July 06, 2017以下文章是閱讀 深入淺出Design Pattern 還有 聖經還有Source making的筆記 圖片截圖自lynda.com的Foundations of Programming: Design Patterns 要更深入的理解一定要去看這兩本書
這篇文章我還用了官方的github code來說明
電視遙控器
把電視想像成一個隨時在接收request的server 今天我們手上有一個很陽春的遙控器 也就是invoker
當我們想把音量調大 就compose一個把音量調大的request讓遙控器client發出request給電視server 要往上轉的時候也是compose一個頻道+1的request給server
這樣實在太麻煩了 很多命令/request我們根本就會一用再用 而且命令本身跟接受方都是不會變的 這種時候比較好的做法是把命令本身實體化 然後這個命令object會被遙控器的特定按鈕trigger 這也是我們今日的遙控器
意思就是 我把 把電視頻道+1 這個命令實體化 可以被遙控器上的 +1 觸發
陽春版
先來實作一個陽春的 這個遙控器一次只能裝載一個命令 只有一個按鈕 每次當你要發不同的命令 就要更新這個遙控器的command variable
public class SimpleRemoteControl {
Command slot;
public SimpleRemoteControl() {}
public void setCommand(Command command) {
slot = command;
}
public void buttonWasPressed() {
slot.execute();
}
}
Command是我們的interface 這個interface長得很簡單
public interface Command {
void execute();
}
所以我們的遙控器裡面有一個variable是command 你可以setCommand之後 按button去execute這個command
因為Command interface有execute這個函式 所以slot可以安心call execute()
電視提供開跟關的功能
public class TV {
public TV() {}
public void on() {
System.out.println("TV is on");
}
public void off() {
System.out.println("TV is off");
}
}
那麼client怎麼call呢
public class RemoteControlTest {
public static void main(String[] args) {
SimpleRemoteControl remote = new SimpleRemoteControl();
TV tv = new TV();
remote.setCommand(tv::on);
remote.buttonWasPressed();
remote.setCommand(tv::off);
remote.buttonWasPressed();
}
}
挺好懂的 執行結果
TV is on
TV is off
code看起來很簡單 但其中有一個奧秘我想特別講一下
上面的code是design pattern的官方github裡的code 馬上可以直接執行 但執行完後 怎麼看都不對勁 為什麼compile會過???
setCommand 吃一個Command 可是tv::on是method reference 我的code從來就沒有說這個method實作了Command 為什麼可以這樣丟進去呢??
有興趣的讀者可以想一想 java初學者如我花了20min 我以後會另外寫一篇functional interface的文章來解釋這個問題
開電視是一個class
開電視這個command變成一個class 我們把遙控器的command設定成這個class的物件
public class TVOnCommand implements Command{
TV tv;
public TVOnCommand(TV tv) {
this.tv = tv;
}
public void execute() {
tv.on();
}
}
Client比剛剛好懂 我需要new一個command的object 丟進setCommand
public class RemoteControlTest {
public static void main(String[] args) {
SimpleRemoteControl remote = new SimpleRemoteControl();
TV tv = new TV();
TVOnCommand tvOn = new TVOnCommand(tv);
remote.setCommand(tvOn);
remote.buttonWasPressed();
}
}
簡單
命令模式
把一個request封裝成一個物件 因為每個物件可以有不同的參數 所以可以客製化對於客戶的請求
結構
-
Command: 命令的interface 所有具體物件需要實作這個interface
-
ConcreteCommand(開電視命令): 將Receiver和相對應的動作連結起來 實作execute
-
Client(你): 建立ConcreteCommand 設定receiver
-
Invoker(遙控器): 要求Command執行命令
-
Receiver(TV): 依據命令執行相對應動作
優缺點
1.請求方和接受方獨立 降低依賴 有新的命令也很容易新增
2.請求本身是一個object 就可以serialize或deserialize去存儲或傳遞
3.可以輕易的支援undo和redo
4.Invoker針對Command介面寫程式 不需要知道ConcreteCommand有幾個或是怎麼實作
5.實作Macro Command容易 可以run time組合任意commands
6.但可能導致過多的命令class 因為所有的command都需要一個class
正常的遙控器
一般的遙控器 每個按鈕都是不同的command request 我們現在來簡單的實作他
遙控器裡面原本只有一個command 現在需要變成command array
我們還實做undo 需要一個變數來記得上一個command是什麼
public class RemoteControl {
Command[] onCommands;
Command[] offCommands;
Command undoCommand;
public RemoteControl() {
onCommands = new Command[3];
offCommands = new Command[3];
Command noCommand = new NoCommand();
for(int i=0;i<3;i++) {
onCommands[i] = noCommand;
offCommands[i] = noCommand;
}
undoCommand = noCommand;
}
public void setCommand(int slot, Command onCommand, Command offCommand) {
onCommands[slot] = onCommand;
offCommands[slot] = offCommand;
}
public void onButtonWasPushed(int slot) {
onCommands[slot].execute();
undoCommand = onCommands[slot];
}
public void offButtonWasPushed(int slot) {
offCommands[slot].execute();
undoCommand = offCommands[slot];
}
public void undoButtonWasPushed() {
undoCommand.undo();
}
}
要實作undo 記得修改Command interface
public interface Command {
public void execute();
public void undo();
}
NoCommand是一個空物件null object 可以當一個interface的default value
public class NoCommand implements Command {
public void execute() { }
public void undo() { }
}
Client的用法也很簡單
1.Create receiver
2.Create ConcreteCommand
3.Set command for each button
public static void main(String[] args) {
RemoteControl remoteControl = new RemoteControl();
Light livingRoomLight = new Light("Living Room");
TV tv= new TV("Living Room");
Stereo stereo = new Stereo("Living Room");
LightOnCommand livingRoomLightOn =
new LightOnCommand(livingRoomLight);
LightOffCommand livingRoomLightOff =
new LightOffCommand(livingRoomLight);
TVOnCommand tvOn =
new TVOnCommand(tv);
TVOffCommand tvOff =
new TVOffCommand(tv);
StereoOnWithCDCommand stereoOnWithCD =
new StereoOnWithCDCommand(stereo);
StereoOffCommand stereoOff =
new StereoOffCommand(stereo);
remoteControl.setCommand(0, livingRoomLightOn, livingRoomLightOff);
remoteControl.setCommand(1, tvOn, tvOff);
remoteControl.setCommand(2, stereoOnWithCD, stereoOff);
remoteControl.onButtonWasPushed(0);
remoteControl.offButtonWasPushed(0);
remoteControl.onButtonWasPushed(1);
remoteControl.offButtonWasPushed(1);
remoteControl.onButtonWasPushed(2);
remoteControl.offButtonWasPushed(2);
remoteControl.undoButtonWasPushed();//支援undo
}
每一個command都是個物件
當然別忘了在實作ConcreteCommand的時候 需要實作undo
public class TVOnCommand implements Command {
TV tv;
public TVOnCommand(TV tv) {
this.tv= tv;
}
public void execute() {
tv.on();
}
public void undo() {
tv.off();
}
}
一鍵懶人包
前菜上完 有趣的來了 把Command具體化的好處還有一個 我們可以把很多個command包成一個MacroCommand
public class MacroCommand implements Command {
Command[] commands;
public MacroCommand(Command[] commands) {
this.commands = commands;
}
public void execute() {
for (int i = 0; i < commands.length; i++) {
commands[i].execute();
}
}
public void undo() {
for (int i = commands.length -1; i >= 0; i--) {
commands[i].undo();
}
}
}
注意這裡undo的順序是反過來的
Client就可以自創MacroCommand
Command[] partyOn = { lightOn, stereoOn, tvOn, hottubOn};
Command[] partyOff = { lightOff, stereoOff, tvOff, hottubOff};
MacroCommand partyOnMacro = new MacroCommand(partyOn);
MacroCommand partyOffMacro = new MacroCommand(partyOff);
remoteControl.setCommand(0, partyOnMacro, partyOffMacro);
remoteControl.onButtonWasPushed(0);
remoteControl.undoButtonWasPushed();
太精彩了 A list of command is still a command!
這樣就可以動態創造你想要的任何combination of commands
總結
命令模式還有一些其他比較常見的用途
1.因為command本身是物件 就很容易用一個command queue存起來 讓receiver慢慢處理
2.還可以把命令序列化 當成log存起來 server掛了的話還可以從disk去redo所有的命令