命令模式筆記-後篇(Command Pattern)

命令模式筆記-後篇

以下定義採自深入淺出設計模式一書:

記得前篇介紹了簡單命令模式,他的圖長這樣:

image

 

記得前篇我們提出的創新需求是什麼:

「將來這個廠牌假如出了一系列的產品,可能包含了5種,那在遙控器上設定五個插槽,每購買一台就附上一個插卡晶片

而遙控器能讓我能自由插上所有我可以購買機器的話。那我不就有一個萬用遙控器了,這種自由度不是更棒嗎?」

 

我們透過完成了命令模式的遙控器API後,我想就可以操控房間裡面的所有電器了吧…

記得插糟嗎?上一篇我們為了操控CDPlayer,所以宣告了CDPlayerOnCommand

那現在假如我有五項電器產品,包含了電腦、遊樂器、電扇、音響以及電燈的話,那該怎麼辦!

計劃是這樣的:我們可以為遙控器的每一個插糟,都對應一個命令,讓遙控器變成Invoker(調用者),當按下遙控器的按鈕以後

就呼叫對應命令物件的execute()方法,而接收者(那些器材)的動作就會被調用

image

這個遙控器可能就是長成這個樣子,前一篇我們只有設計一個slot,讓CDPlayer插入他的晶片。

直覺上,我們可以透過陣列來存放這一些不同的器材與設備。然後以類似的方式,將命令指定給遙控器,像下面一樣:

   1: onCommands[0] = onCommand;
   2: offCommands[0] = offCommand;

陣列這麼好用的話,我們來實踐遙控器吧!

   1:  
   2:  
   3:     public interface Command{
   4:         public void execute();
   5:         public string getClass();
   6:     }
   7:  
   8:     
   9:     public class MyRoomController
  10:     {
  11:         Command[] onCommands;
  12:         Command[] offCommands;
  13:         public MyRoomController()
  14:         {
  15:             //共有五個插糟
  16:             onCommands = new Command[5];
  17:             offCommands = new Command[5];
  18:  
  19:             Command noCommand = new NoComand();
  20:  
  21:             for (int i = 0; i < 5; i++)
  22:             {
  23:                 onCommands[i] = noCommand;
  24:                 offCommands[i] = noCommand;
  25:             }
  26:         }
  27:  
  28:         public void setCommand(int slot, Command onCommand, Command offCommand)
  29:         {
  30:             onCommands[slot] = onCommand;
  31:             offCommands[slot] = offCommand;
  32:         }
  33:  
  34:         public void onButtonWasPushed(int slot)
  35:         {
  36:             onCommands[slot].execute();
  37:         }
  38:  
  39:         public void offButtonWasPushed(int slot)
  40:         {
  41:             offCommands[slot].execute();
  42:         }
  43:  
  44:         public string toString()
  45:         {
  46:             //改寫ToString()印出插糟名稱與對應的命令!
  47:             StringBuilder sb = new StringBuilder();
  48:                                 sb.Append("\n---------遙控器--------\n");
  49:                                 for (int i = 0; i < 5; i++)
  50:                                 {
  51:                                     sb.Append("[Slot"+ i +"]"+onCommands[i].getClass()+"    " + offCommands[i].getClass()+"\n");
  52:                                 }
  53:  
  54:                     return sb.ToString();
  55:                 }
  56:     }

別忘了,遙控器也只是一個調用者,我們先來實踐一些接收者吧!

我們先實踐介面可能最單純的燈物件

   1: //例如這個燈物件
   2: public class Light{
   3:     string Location;
   4:     public Light(string Location){
   5:         this.Location = Location;
   6:     }
   7:  
   8:     public void getName(){
   9:         return this.Location();
  10:     }
  11:  
  12:     public void off(){
  13:         Console.Write(this.Location + "的燈被關了\n");
  14:     }
  15:  
  16:     public void on(){
  17:         Console.Write(this.Location + "的燈被開了\n");
  18:     }
  19: }

延續前篇,我們要實踐一個命令,讓人調用,所以也要宣告一個實踐Command介面的一個命令出來喲

   1: //燈就簡單多了,只有開跟關,當然你也可以寫一個更複雜,更多元的燈
   2: public class LightOnCommand : Command
   3: {
   4:     Light light;
   5:  
   6:     public LightOnCommand(Light light){
   7:         this.light = light;
   8:     }
   9:  
  10:     public void execute(){
  11:         this.light.on();
  12:     }
  13:  
  14:     public Light getClass()
  15:     {
  16:         return this.light;
  17:     }
  18: }

那音響呢?這樣也要寫一個音響物件出來耶…沒錯,當你可以寫出來以後,相信你已經有能力可以完成你房間內所有電器的自動化的命令了!

   1: //音響介面
   2: public class Stereo{
   3:     string Location;
   4:  
   5:     public Stereo(string  Location)
   6:     {
   7:         this.Location = Location;
   8:     }
   9:  
  10:     public void getName()
  11:     {
  12:         return this.Location() + "的音響";
  13:     }
  14:  
  15:     public void off(){
  16:         Console.Write("音響關了\n");
  17:     }
  18:  
  19:  
  20:     public void on(){
  21:         Console.Write("音響開了\n");
  22:     }
  23:  
  24:     
  25:     public void setVolume(int n){
  26:         Console.Write("音量調成了"+n.ToString()+"\n");
  27:     }
  28:  
  29:     
  30:     public void setCD(string cd){
  31:         Console.Write("放入了"+cd+"的CD\n");
  32:     }
  33: }

好了,先這樣,我想,還不用測試,你就知道結果了吧!那我們測試先暫緩

剛剛onButtonWasPushed()的函式中,可能會需要這樣的程式碼:

   1: public void onButtonWasPushed(int slot)
   2: {
   3:     if (onCommands[slot]!=null)
   4:         onCommands[slot].execute();
   5: }
   6:  
   7: //空物件
   8: public class NoCommand:Command
   9: {
  10:     public void execute(){}
  11:     public void getClass(){
  12:         Console.write("沒有命令");
  13:     }    
  14: }
  15:  
  16: //建構式中
  17: Command noCommand = new NoComand();
  18:  for (int i = 0; i < 5; i++)
  19:  {
  20:      onCommands[i] = noCommand;
  21:      offCommands[i] = noCommand;
  22:  }

註:上面實踐了一種模式,NoCommand是一個空物件(null object),當需要一個沒有任何意義值時,空物件就很有用

Client也可將處理Null的責任轉移給空物件。

在這邊為例,遙控器不可能一出廠就知道你家有哪些電器需要命令來設定,所以提供了NoCommand物件作為代用品

他的責任就是,被執行到execute()的時候,什麼也不做,很多時候,空物件本身也是一種設計模式!

 

另外命令模式中還有一個特色要好好給他介紹一下:

命令是可以復原的!例如,電燈開了以後要復原他的命令,自然是把燈關閉了!

那關閉了音響以後,復原命令自然就是開啟音響囉!

既然命令都可以復原,那麼我們應該怎麼做呢??

 

在命令模式中實在是再簡單不多了!去針對介面寫程式吧!

   1: public interface Command{
   2:     public void execute();
   3:     public void undo();
   4:  
   5:     public string getClass();
   6: }

相對的,命令物件就要這樣做:

   1: //燈就簡單多了,只有開跟關,當然你也可以寫一個更複雜,更多元的燈
   2: public class LightOnCommand : Command
   3: {
   4:     Light light;
   5:  
   6:     public LightOnCommand(Light light){
   7:         this.light = light;
   8:     }
   9:  
  10:     public void execute(){
  11:         this.light.on();
  12:     }
  13:  
  14:     public void undo()
  15:     {
  16:         this.light.off();
  17:     }
  18:  
  19:     public string getClass()
  20:     {
  21:         return this.light.getName();
  22:     }
  23: }

相信你也知道LightOffCommand要怎麼實作了。

為了因應這樣復原按鈕的支援,所以,必須對遙控器類別作一些修改(看綠色的註解處)

   1: public class MyRoomController
   2: {
   3:     Command[] onCommands;
   4:     Command[] offCommands;
   5:     //復原功能的小修改
   6:     Command undoCommand;
   7:  
   8:     public MyRoomController()
   9:     {
  10:         onCommands = new Command[5];
  11:         offCommands = new Command[5];
  12:         
  13:  
  14:         Command noCommand = new NoComand();
  15:  
  16:         for (int i = 0; i < 5; i++)
  17:         {
  18:             onCommands[i] = noCommand;
  19:             offCommands[i] = noCommand;
  20:         }
  21:         //復原功能的小修改
  22:         undoCommand = noCommand();
  23:     }
  24:  
  25:     public void setCommand(int slot, Command onCommand, Command offCommand)
  26:     {
  27:         onCommands[slot] = onCommand;
  28:         offCommands[slot] = offCommand;
  29:     }
  30:  
  31:     public void onButtonWasPushed(int slot)
  32:     {
  33:         onCommands[slot].execute();
  34:         //復原功能的小修改
  35:         undoCommand = onCommands[slot];
  36:     }
  37:  
  38:     public void offButtonWasPushed(int slot)
  39:     {
  40:         offCommands[slot].execute();
  41:         //復原功能的小修改
  42:         undoCommand = offCommands[slot];
  43:     }

當你所控制的物件狀態只有兩種的時候,自然不會有問題,就是用上上面的方式來處理命令的實踐(Light.on or off)

但是假如你有先前執行的狀態的時候,通常想要實踐這樣的功能,需要去紀錄一些狀態,例如,Office的復原或是下一步的按鈕

想要回到上一步或下一步,就務必記錄下來狀態,因此,我們這時候,可以將物件的狀態變數,記錄在命令類別中。

例如:

   1: public class ElectronicFanSpeedHighCommand():Command
   2: {
   3:    ElectronicFan electronicfan;
   4:    //記錄狀態的變數。
   5:       string prevSpeed;
   6:  
   7:    public ElectronicFanSpeedHighCommand(ElectronicFan ElectronicFan){
   8:               this.electronicfan = ElectronicFan;
   9:     } 
  10:  
  11:       public void execute(){
  12:              prevSpeed = electronicfan.getSpeed();//寫在風扇的物件方法中
  13:             electronicfan.speedHigh(); //改變速度之前,要像上一行一樣記錄狀態。
  14:      } 
  15:  
  16:     public void undo(){
  17:         if (prevSpeed == electronicfan.HIGH) //常數
  18:              electronicfan.speedHigh();
  19:         if (prevSpeed == electronicfan.LOW) //常數 
  20:              electronicfan.speedLow();
  21:         //以此類推
  22:               }
  23: }

 

命令模式很容易的可以加入復原功能吧…而且將來需求又來的時候,只需要改到命令的實作類別而己。

 

然而…遙控器會不會夢想太小呢?

既然都已經是萬用遙控器了,那家庭自動化如何呢??

試想按下一個按鈕,就同時關燈,打開音響和電視,設定好DVD,並讓冷氣打開

或者是現在許多會議室都配置了自動化環境設定,可以快速進入會議模式、簡報模式或是上課模式的話

那這樣啟不是完美嗎?至少省去了很多試開關的動作呢…。

 

再回到Office的應用…「巨集」就是這樣的一個概念。將一連串的動作都”錄”下來,然後依序去執行,甚至…可以復原?

哇嗚,來實踐看看

   1: //巨集,一次不只執行一個命令,不錯吧
   2: public class MacroCommand : Command
   3: {
   4:     Command[] commands;
   5:     public MacroCommand(Command[] commands)
   6:     {
   7:         this.commands = commands;
   8:     }
   9:  
  10:     public void execute()
  11:     {
  12:         //依序執行命令
  13:         for (int i = 0; i < commands.Length; i++)
  14:         {
  15:             commands[i].execute();
  16:         }
  17:     }
  18: }

 

測試的時候,就如以下方式來建立”命令”巨集,下面例子是建立音樂廳模式,然後將這個模式設定到遙控器上!

   1: //音樂廳模式:關燈後,又開音響...以此類推
   2:  
   3: //建立所有的裝置
   4: Light light = new Light("我房間");
   5: Stereo stereo = new Stereo("我房間");
   6: //...以此類推
   7:  
   8:  
   9: //建立所有命令
  10: LightOffCommand lightOff = new LightOffCommand(light);
  11: StereoOnWithCDCommand stereoOn = new StereoOnWithCDCommand(stereo);
  12: //...以此類推
  13:  
  14:  
  15: //建立巨集陣列
  16: Command[] concertModeOn = { lightOff, stereoOn };
  17: Command[] concertModeOff = { lightOn, stereoOff };
  18: MacroCommand concertModeOnMacro = new MacroCommand(concertModeOn);
  19: MacroCommand concertModeOffMacro = new MacroCommand(concertModeOff);
  20:  
  21: //設定給遙控器按鈕
  22: MyRoomController Controller = new MyRoomController();
  23: Controller.setCommand(0, concertModeOnMacro, concertModeOffMacro);

最後,巨集命令可以復原嗎?當然可以

既然建立了Interface,而且規範了undo方法。

你只需要在MacroCommand中,建立一個Undo()函式,然後跟執行一樣,去逐一復原每一道命令。

 

後記:

命令模式見到了他的本尊,可想而知他的靈活之處,這個模式將發出請求的物件與執行請求的物件鬆綁,而且命令物件可以一個或是一組動作。

只是很常見的,我們通常習慣在一個函式中,將所有的請求執行完,而不是將工作交由接收者(例如燈)去進行,當然這是個聰明(複雜笨重)的物件實踐行為

但這個物件就被緊緊的綁住了,只是如此一來,也不行能把接收者當作參數傳給命令物件。

最後的最後,再來複習一下命令模式吧:

命令模式:將請求封裝成物件,這可以讓你使用不同的請求、佇列、或者是日誌,來參數化其他物件。命令模式也可以支援復原操作。

 

註:

命令模式還有更多的用途:例如佇列的請求(網頁伺服器、排程器、執行緒池等…),工作佇列的應用,可以將實踐命令介面的物件放到佇列後

有效地去將執行緒的運算限制在固定數目中進行,如此一來,這樣可以有效的管理運算資源的使用。

還有日誌請求(特別是例如Office軟體這類的大型應用程式),這些都擴充了命令模式。

例如日誌的實作,通常調用了大型資料結構的動作,而藉由日誌記錄所有操作,如果系統出狀況,從一個CheckPoint

再重新採用這些操作。而不是每一個變動後,就記錄整個資訊(例如試算表、簡報等),這些技術後續被擴充採用交易機制(Transaction)