設計模式之命令模式(二)

  • 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,如何破解,敬請期待我們的下一篇。