HeadFirst設計模式(六) - 命令模式

命令模式的目的

    咱們將把封裝帶到一個全新的世界,即把方法調用(method invocation)封裝起來。沒錯,經過封裝方法調用,咱們能夠把運算塊包裝成形。因此調用此運算的對象不須要關心事情是如何進行的,只要知道如何使用包裝成形的方法來完成它就能夠。經過封裝方法調用,也能夠作一些很聰明的事情,例如記錄日誌,或者重複使用這些封裝來實現撤銷(undo)。java

舉個例子

    要實現一個多功能的遙控器,這個遙控器具備七個可編程的插槽(每一個均可以指定到一個不一樣的加點裝置),每一個插槽都有對應的開關按鈕。這個遙控器還具有一個總體的撤銷按鈕。編程

    目前已經有了許多廠商開發出來的Java類,用來控制家電自動化裝置,例如電燈、風扇、熱水器、音響設備和其餘相似的可控制裝置。函數

    建立一組控制遙控器的API,讓每一個插槽都可以控制一個或一組裝置。請注意,可以控制目前的裝置和將來可能出現的裝置,這一點是很重要的。測試

分析這個例子

    目前已知的類是這些廠家提供的Java類,讓咱們來看看他們的設計:this

    固然,類不僅這幾個,可是相差不大,許多的類都具備on()和off()方法。但除此以外,還有一些其餘方法。並且聽起來彷佛未來還會有更多的廠商類,並且每一個類還會有各類各樣的新方法。spa

    對於遙控器而言,須要關注的是如何解讀按鈕被按下的動做,而後發出正確的請求,可是遙控器不須要知道這些家電自動化的細節。線程

    不要讓遙控器包含一大堆if語句,例如:設計

if slot1 == Light then light.on()
else if slo1 == .....

    若是這樣,只要有新的廠商類進來,就必須修改代碼,這會形成潛在的錯誤,並且工做沒完沒了。日誌

    對於這種狀況,咱們就可使用命令模式,命令模式可將「動做的請求者」從「動做的執行者」對象中解耦。在上面的例子中,請求者能夠是遙控器,而執行者對象就是廠商類其中之一的實例。利用命令模式,把請求(例如打開電燈)封裝成一個特定的對象(例如客廳電燈對象)。因此若是對每一個按鈕都存儲一個命令對象,那麼當按鈕被按下的時候,就能夠請命令對象作相關的工做。遙控器並不須要知道工做內容是什麼,只要有個命令對象能和正確的對象溝通,把事情作好就能夠了。code

    說了這麼多好像有些混亂,讓咱們用代碼來實現。

第一個命令對象

    首先,實現命令接口,讓全部的命令對象實現相同的包含一個方法的接口:

public interface Command {
	public void execute();
}

    接下來,實現一個打開電燈的命令。根據廠商提供的類,Light類有兩個方法,即on()和off()方法。下面是如何將他們實現成一個命令的代碼:

/**
 * 打開電燈的命令
 */
public class LightOnCommand implements Command {

	// 電燈對象
	private Light light;

	// 構造函數,傳入一個電燈對象
	public LightOnCommand(Light light) {
		this.light = light;
	}
	
	// 命令方法的執行函數,這裏將打開電燈
	public void execute() {
		light.on();
	}

}

    讓咱們看看這個類都作了那些事情:

  1. 該類實現了Command接口,如今它是一個命令對象;
  2. 構造函數要求該類在實例時要傳入某個電燈(比方說客廳的電燈),以便讓這個命令控制,而後記錄在實例變量light中。一旦調用execute(),就有這個電燈對象成爲接收者負責接受請求;
  3. execute(0方法調用接收對象的on()方法;

    如今有了LightOnCommand類,讓咱們看看如何使用它。

使用命令對象

    建立一個遙控器類,它目前只有一個按鈕和對應的插槽,能夠控制一個裝置:

public class SimpleRemoteControl {
	// 只有一個插槽
	Command slot;
	
	// 構造函數
	public SimpleRemoteControl() {}
	
	// 設置插槽要執行的命令
	public void setCommand(Command command) {
		this.slot = command;
	}
	
	// 按下遙控器的按鈕,這個方法就會被調用
	public void buttonWasPressed() {
		slot.execute();
	}
}

    該類有一個成員變量slot,它是類型是一個Command接口,在實例化這個類時須要被傳入。而後在調用buttonWasPressed()方法時,會調用這個命令接口的execute()方法。如今,對這個例子進行測試:

public class Client {

	public static void main(String[] args) {
		// 建立一個遙控器
		SimpleRemoteControl remote = new SimpleRemoteControl();

		// 建立一個電燈對象
		Light light = new Light();
		// 建立一個開燈命令對象,將電燈傳入給它
		LightOnCommand lightOn = new LightOnCommand(light);

		// 將該命令輸入到遙控器的按鈕中
		// 遙控器只有一個按鈕,按下這個按鈕就會執行lightOn命令對象的execute()方法
		remote.setCommand(lightOn);

		// 按下遙控器按鈕
		remote.buttonWasPressed();

		/**
		 * output:打開電燈.
		 */
	}

}

命令模式的定義

命令模式將「請求」封裝成對象,以便使用不一樣的請求、隊列或者日誌來參數化其餘對象。命令模式也支持可撤銷的操做。

    如今,仔細看這個定義。咱們知道一個命令對象經過在特定接收者上綁定一組動做來封裝一個請求。要打到這一點,命令對象將動做和接收者包裝進對象中。這個對象只暴露出一個execute()方法,當此方法被調用的時候,接收者就會進行這些動做。從外面看來,其餘對象不知道究竟哪一個接收者進行了哪些動做,只知道若是調用execute()方法,請求的目的就能達到。

命令模式的類圖

    若是將上面例子中的類套用到該類圖中的話,具體以下:

  • Command接口就是例子中的Command接口;
  • ConcreteCommand(具體命令)類就是例子中的LightOnCommand類;
  • Receiver(接收者)類就是例子中的Light類;
  • Invoker(調用者)就是例子中的SimpleRemoteControl類;
  • Client就是例子中用於測試的Client類;

完成這個遙控器例子

首先編寫Receiver類:

public class Light {
	public void on() {
		System.out.println("打開電燈.");
	}
	
	public void off() {
		System.out.println("關閉電燈.");
	}
}

public class Stereo {
	public void on() {
		System.out.println("打開音響.");
	}
	
	public void off() {
		System.out.println("關閉音響.");
	}
}

public class TV {
	public void on() {
		System.out.println("打開電視.");
	}
	
	public void off() {
		System.out.println("關閉電視.");
	}
}

接着編寫Command接口:

public interface Command {
	public void execute();
	public void undo();
}

接着編寫ConcreteCommand類:

public class NoCommand implements Command {

	public void execute() {
		System.out.println("按鈕無效.");
	}

}

public class LightOnCommand implements Command {

	// 電燈對象
	private Light light;

	// 構造函數,傳入一個電燈對象
	public LightOnCommand(Light light) {
		this.light = light;
	}
	
	// 命令方法的執行函數,這裏將打開電燈
	public void execute() {
		light.on();
	}
	
	public void undo(){
		light.off();
	}
}

public class LightOffCommand implements Command {
	// 電燈對象
	private Light light;

	// 構造函數,傳入一個電燈對象
	public LightOffCommand(Light light) {
			this.light = light;
		}

	// 命令方法的執行函數,這裏將關閉電燈
	public void execute() {
		light.off();
	}
	
	public void undo(){
		light.on();
	}
}

public class StereoOnCommand implements Command {
	// 音響對象
	private Stereo stereo;

	// 構造函數,傳入一個音響對象
	public StereoOnCommand(Stereo stereo) {
		this.stereo = stereo;
	}

	// 命令方法的執行函數,這裏將打開音響
	public void execute() {
		stereo.on();
	}
	
	public void undo(){
		stereo.off();
	}
}

public class StereoOffCommand implements Command {
	// 音響對象
	private Stereo stereo;

	// 構造函數,傳入一個音響對象
	public StereoOffCommand(Stereo stereo) {
		this.stereo = stereo;
	}

	// 命令方法的執行函數,這裏將關閉音響
	public void execute() {
		stereo.off();
	}
	
	public void undo(){
		stereo.on();
	}
}

public class TVOnCommand implements Command {
	// 電視對象
	private TV tv;

	// 構造函數,傳入一個電視對象
	public TVOnCommand(TV tv) {
		this.tv = tv;
	}

	// 命令方法的執行函數,這裏將打開電視
	public void execute() {
		tv.on();
	}
	
	public void undo(){
		tv.off();
	}
}

public class TVOffCommand implements Command {
	// 電視對象
	private TV tv;

	// 構造函數,傳入一個電視對象
	public TVOffCommand(TV tv) {
			this.tv = tv;
		}

	// 命令方法的執行函數,這裏將關閉電視
	public void execute() {
		tv.off();
	}
	
	public void undo(){
		tv.on();
	}
}

接着編寫Invoker類:

public class RemoteControl {
	// on的命令組
	private Command[] onCommands;
	// off的命令組
	private Command[] offCommands;
	// 撤銷命令
	private Command undoCommand;

	public RemoteControl() {
		// 初始化時將命令都設置成noCommand
		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實例變量中。
		// 不論是開仍是關,咱們處理方法都是同樣的。
		undoCommand = onCommands[slot];
	}

	public void offbuttonWasPushed(int slot) {
		offCommands[slot].execute();
		// 當按下按鈕時,取得這個命令,記錄在undoCommand實例變量中。
		// 不論是開仍是關,咱們處理方法都是同樣的。
		undoCommand = offCommands[slot];
	}

	public void undoButtonWasPushed() {
		undoCommand.undo();
	}
}

最後編寫測試代碼:

public class Client {

	public static void main(String[] args) {
		// 建立遙控器,也就是Invoker
		RemoteControl remote = new RemoteControl();
		
		// 建立具體的設備,也就是Receiver
		Light light = new Light();
		TV tv = new TV();
		Stereo stereo = new Stereo();
		
		// 建立具體的命令,也就是ConcreteCommand
		// 開燈關燈的命令
		LightOnCommand lightOn = new LightOnCommand(light);
		LightOffCommand lightOff = new LightOffCommand(light);
		// 開電視關電視的命令
		TVOnCommand tvOn = new TVOnCommand(tv);
		TVOffCommand tvOff = new TVOffCommand(tv);
		// 開音響關音響的命令
		StereoOnCommand stereoOn = new StereoOnCommand(stereo);
		StereoOffCommand stereoOff = new StereoOffCommand(stereo);
		
		// 將命令裝載到遙控器中
		remote.setCommand(0, lightOn, lightOff);
		remote.setCommand(1, tvOn, tvOff);
		remote.setCommand(2, stereoOn, stereoOff);
		
		// 測試開的按鈕
		remote.onButtonWasPushed(0);
		remote.onButtonWasPushed(1);
		remote.onButtonWasPushed(2);
		
		// 測試關的按鈕
		remote.offbuttonWasPushed(0);
		remote.offbuttonWasPushed(1);
		remote.offbuttonWasPushed(2);
		
		// 測試撤銷的按鈕
		// 最後按的是關閉音響,那麼執行該方法後,音響會開啓
		remote.undoButtonWasPushed();
	}

}

輸出結果:

打開電燈. // 測試on按鈕
打開電視.
打開音響.
關閉電燈. // 測試off按鈕
關閉電視.
關閉音響.
打開音響. // 測試undo按鈕

到此位置,測試結束!

關於一些細節

問:接收者必定有必要存在嗎?爲什麼命令對象不直接實現execute()方法的細節?

答:通常來講,儘可能設計傻瓜命令對象,它只懂得調用一個接收者的一個行爲(單一職責)。然而,有許多「聰明」的命令對象會實現許多邏輯,直接完成一個請求,但耦合程度高。

問:如何可以實現多層次的撤銷操做?但願按下撤銷按鈕許屢次,回到很早之前狀態。

答:不要只是記錄最後一個命令,而使用一個堆棧(後進先出)記錄操做過的每個命令。而後,無論何時按下了撤銷按鈕,你均可以從堆棧中取出最上層的命令,而後執行undo()方法來撤銷它。

最後說一些其餘的,命令模式還能夠有更多的用途,好比使用在隊列請求和日誌請求。

想象一下,有一個工做隊列(先進先出),你在某一端添加命令,而後另外一端則是線程,線程從隊列中取出一個命令,而後調用它的execute()方法,等待這個調用完成,而後丟棄該命令,執行下一個……

在想象一下,某些應用須要咱們將全部的動做都記錄在日誌中,並能在系統死機後,從新調用這些動做恢復到以前的狀態。咱們能夠在執行命令的時候,將歷史記錄存儲在磁盤中。一旦系統死機重啓後,咱們就能夠將命令對象讀取出來從新執行execute()方法。

命令模式能用到地方還有不少,目前就記錄到這裏。

相關文章
相關標籤/搜索