designPattern

Design Pattern(11) - Command

以下文章是閱讀 深入淺出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();
  }
}

簡單

Alt text ### 命令模式

把一個request封裝成一個物件 因為每個物件可以有不同的參數 所以可以客製化對於客戶的請求

結構

Alt text

優缺點

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所有的命令