設計模式之命令模式(二)
- 2019 年 12 月 25 日
- 筆記
上一次留給大家去做的實踐,不知道大家執行的怎麼樣了呢。
我們通過一個簡單的練習,完成了一個控制開關。那現在,我們打算將遙控器的每個插槽,對應到一個命令這樣就要遙控器變成「調用者」。當按下按鈕,相應命令對象的execute()方法就會被調用,其結果就是,接收者(例如電燈、風扇、音響)的動作被調用。
實現遙控器
public class RemoteControl { Command[] onCommands; Command[] offCommands; public RemoteControl() { onCommands = new Command[7]; offCommands = new Command[7]; // 在構造器中,只需實例化並初始化這兩個開與關的數組 Command noCommand = new NoCommand(); for (int i = 0; i < 7; i++) { onCommands[i] = noCommand; offCommands[i] = noCommand; } } // 這個方法有三個參數,分別是插槽的位置、開的命令、關的命令。這些命令將記錄開關數組中對應的插槽位置,以供稍後使用 public void setCommand(int slot, Command onCommand, Command offCommand) { onCommands[slot] = onCommand; offCommands[slot] = offCommand; } // 當按下開或關的按鈕,硬體就會負責調用對應的方法,也就是onButtonWasPushed或offButtonWasPushed public void onButtonWasPushed(int slot) { onCommands[slot].execute(); } public void offButtonWasPushed(int slot) { offCommands[slot].execute(); } public String toString() { StringBuffer stringBuff = new StringBuffer(); stringBuff.append("n------ Remote Control -------n"); for (int i = 0; i < onCommands.length; i++) { stringBuff.append("[slot " + i + "] " + onCommands[i].getClass().getName() + " " + offCommands[i].getClass().getName() + "n"); } return stringBuff.toString(); } }
實現命令
此前我們已經動手實現過LightOnCommand,純粹就是簡單的開和關命令。那現在,我們來為音響編寫開與關的命令。
音響的關閉是毫無難度,就是開啟的時候有點複雜,你知道為什麼嗎?難道音響開了就好了?是否還需要後續其他的動作才能讓音響響起來了?哎呀,小編多嘴了好像。
public class StereoOnWithCDCommand implements Command { Stereo stereo; public StereoOnWithCDCommand(Stereo stereo) { this.stereo = stereo; } // 打開音響,需要三個步驟,開啟音響,設置CD播放,設置音量,不然就成啞巴了 public void execute() { stereo.on(); stereo.setCD(); stereo.setVolume(11); } }
這裡列舉了一個電燈,一個音響,差不多就把其他類似的都已經搞定了,比如電扇、門,對吧。所以,趕緊看看你之前動手的操作,是不是和小編的差不多。
讓我們繼續看下,多個的是怎麼實現的呢。
public class RemoteLoader { public static void main(String[] args) { RemoteControl remoteControl = new RemoteControl(); // 將所有的裝置創建在合適的位置 Light livingRoomLight = new Light("Living Room"); Light kitchenLight = new Light("Kitchen"); CeilingFan ceilingFan= new CeilingFan("Living Room"); GarageDoor garageDoor = new GarageDoor(""); Stereo stereo = new Stereo("Living Room"); // 創建所有的電燈命令對象 LightOnCommand livingRoomLightOn = new LightOnCommand(livingRoomLight); LightOffCommand livingRoomLightOff = new LightOffCommand(livingRoomLight); LightOnCommand kitchenLightOn = new LightOnCommand(kitchenLight); LightOffCommand kitchenLightOff = new LightOffCommand(kitchenLight); // 創建吊扇的開與關命令 CeilingFanOnCommand ceilingFanOn = new CeilingFanOnCommand(ceilingFan); CeilingFanOffCommand ceilingFanOff = new CeilingFanOffCommand(ceilingFan); // 創建車庫門的上與下命令 GarageDoorUpCommand garageDoorUp = new GarageDoorUpCommand(garageDoor); GarageDoorDownCommand garageDoorDown = new GarageDoorDownCommand(garageDoor); // 創建音響的開與關命令 StereoOnWithCDCommand stereoOnWithCD = new StereoOnWithCDCommand(stereo); StereoOffCommand stereoOff = new StereoOffCommand(stereo); // 現在已經有了全部的命令,我們將它們載入到遙控器插槽中 remoteControl.setCommand(0, livingRoomLightOn, livingRoomLightOff); remoteControl.setCommand(1, kitchenLightOn, kitchenLightOff); remoteControl.setCommand(2, ceilingFanOn, ceilingFanOff); remoteControl.setCommand(3, stereoOnWithCD, stereoOff); System.out.println(remoteControl); // 在這裡逐步按下每個插槽的開與關按鈕 remoteControl.onButtonWasPushed(0); remoteControl.offButtonWasPushed(0); remoteControl.onButtonWasPushed(1); remoteControl.offButtonWasPushed(1); remoteControl.onButtonWasPushed(2); remoteControl.offButtonWasPushed(2); remoteControl.onButtonWasPushed(3); remoteControl.offButtonWasPushed(3); } }
寫文檔的時候到了
我們這個主要的設計目標就是讓遙控器程式碼儘可能地簡單,這樣一來,新的廠商類一旦出現,遙控器並不需要隨之修改。因為,我們才用了命令模式,從邏輯上將遙控器的類和廠商的類解耦。我們相信這將降低遙控器的生產成本,並大大地減少維護時所需的費用。
下面的類圖提供了設計的全貌:
撤銷哪去了?
別急別急,小編說的功能都會有的。撤銷功能使用起來就是這樣的:比如說客廳的電燈是關閉的,然後你按下遙控器上的開啟按鈕,自然電燈就被打開了。現在如果按下撤銷按鈕,那麼上一個動作將被倒轉,在這個例子里,電燈將被關閉。
同樣,我們先來一個簡單的撤銷示例。之前我們用的是execute()方法實現開啟或者關閉的調用,那麼我們用undo()方法來執行撤銷操作。即在Command介面里實現一個同execute()相反的方法undo(),然後在實現類里將undo()的動作做成和execute()相反的操作即可。
講的有點籠統?在這裡小編就不提供具體的程式碼了,詳細的請看GitHub我的分享吧。
使用狀態實現撤銷
因為電燈這個開關已經撤銷,是很簡單的入門,小編沒有提供源碼在文中,但是因為還有電風扇這個存在,小編還不得不繼續搞一個高大上的方式。電扇不僅僅是開關,還有檔位的存在,對吧,是不是瞬間有思路了呢?
public class CeilingFan { public static final int HIGH = 3; public static final int MEDIUM = 2; public static final int LOW = 1; public static final int OFF = 0; String location; int speed; public CeilingFan(String location) { this.location = location; speed = OFF; } public void high() { speed = HIGH; System.out.println(location + " ceiling fan is on high"); } public void medium() { speed = MEDIUM; System.out.println(location + " ceiling fan is on medium"); } public void low() { speed = LOW; System.out.println(location + " ceiling fan is on low"); } public void off() { speed = OFF; System.out.println(location + " ceiling fan is off"); } public int getSpeed() { return speed; } }
現在我們就來實現風扇的撤銷。這麼做,需要追蹤吊扇的最後設置速度,如果undo方法被調用了,就要恢復成之前吊扇速度的設置值。就如下面這樣:
public class CeilingFanHighCommand implements Command { CeilingFan ceilingFan; // 增加局部狀態以便追蹤吊扇之前的速度 int prevSpeed; public CeilingFanHighCommand(CeilingFan ceilingFan) { this.ceilingFan = ceilingFan; } public void execute() { // 我們改變吊扇的速度之前,需要先將它之前的狀態記錄起來,以便需要撤銷時使用 prevSpeed = ceilingFan.getSpeed(); ceilingFan.high(); } // 將吊扇的速度設置會之前的值,達到撤銷的目的 public void undo() { if (prevSpeed == CeilingFan.HIGH) { ceilingFan.high(); } else if (prevSpeed == CeilingFan.MEDIUM) { ceilingFan.medium(); } else if (prevSpeed == CeilingFan.LOW) { ceilingFan.low(); } else if (prevSpeed == CeilingFan.OFF) { ceilingFan.off(); } } }
讓我們來測試下風扇吧
條件都具備了,那我們來測試下吧。我們打算把0號插槽的開啟按鈕設置為中速,把第1號插槽的開啟按鈕設置成高速,程式碼如下:
public class RemoteLoader { public static void main(String[] args) { RemoteControlWithUndo remoteControl = new RemoteControlWithUndo(); CeilingFan ceilingFan = new CeilingFan("Living Room"); CeilingFanMediumCommand ceilingFanMedium = new CeilingFanMediumCommand(ceilingFan); CeilingFanHighCommand ceilingFanHigh = new CeilingFanHighCommand(ceilingFan); CeilingFanOffCommand ceilingFanOff = new CeilingFanOffCommand(ceilingFan); remoteControl.setCommand(0, ceilingFanMedium, ceilingFanOff); remoteControl.setCommand(1, ceilingFanHigh, ceilingFanOff); // 首先,我們以中速開啟吊扇 remoteControl.onButtonWasPushed(0); // 然後關閉 remoteControl.offButtonWasPushed(0); System.out.println(remoteControl); // 撤銷,應該會回到中速 remoteControl.undoButtonWasPushed(); // 這個時候開啟高速 remoteControl.onButtonWasPushed(1); System.out.println(remoteControl); // 再進行一次撤銷,應該會回到中速 remoteControl.undoButtonWasPushed(); } }
好了,至此我們不僅僅實現了單個的開與關,還實現了一整個遙控器所有控制項的開與關,甚至是複雜的家電的開與關(音響、電扇的開啟略複雜),而且均實現了撤銷。作為程式設計師的你是不是經常使用撤銷功能呢,反正我是經常使用的噢。
但是,這還不是終極狀態。我們在這裡只能實現一個家電的開與關,如果光憑按下一個按鈕,不能實現燈光、電視、音響的同步使用,那這個遙控器對我們來說是不是還是有點low呢?是吧,確實有點low,如何破解,敬請期待我們的下一篇。