【學習筆記javascript設計模式與開發實踐(命令模式)----9】

第9章 命令模式

命令模式中的命令指的是一個執行某些特定事情的指令。

最常見的應用場景:有時候需要向某些對象發送請求,但又不知道請求的接收者是誰,也不知道被請求的操作是什麼。此時希望用一種鬆耦合的方式來設計程序,使得請求發送者和請求接收者能夠消除彼此之間的耦合關係。

9.2 命令模式的例子-----菜單程序

界面上有數十個按鈕,因爲項目比較複雜,所以我們決定讓某個程序員負責繪製這些按鈕,而另外一些程序員則負責點擊後的具體行爲,這些行爲都將被封裝在對象裏。在大型的項目開發中,這是很正常的分工。對於繪製按鈕的程序員來說,他完全不知道某個按鈕將來做什麼,可能用來刷新菜單界面,也可能是來增加一些子菜單,他只知道點擊這個按鈕會發生某些事情。那麼當完成這個按鈕的繪製之後,應該如何給它綁定onclick事件呢?

回憶命令模式的使用場景:

有時候需要向某些對象發送請求,但又不知道請求的接收者是誰,也不知道被請求的操作是什麼。此時希望用一種鬆耦合的方式來設計程序,使得請求發送者和請求接收者能夠消除彼此之間的耦合關係

我們很快可以找到這裏運用命令模式的理由:點擊了按鈕之後,必須向某些負責具體行爲的對象發送請求。這些對象就是請求的接收者。但目前並不知道接收者是什麼對象,也不知道接收者究竟會做什麼。此時我們需要藉助命令對象的幫助,以便解開按鈕和負責具體行爲的對象間的耦合。

設計模式的主題總是把不變的事物和變化的事物分離開來,命令模式也不例外。按下按鈕之後會發生一些事情是不變的,而具體會發生什麼事情是可變的。通過command對象的幫助,將來我們可以輕易地改變這種關係。

畫界面的同學:

<body>
<button id=’buttons1’>點擊按鈕1</button>
<button id=’buttons2’>點擊按鈕2</button>
<button id=’buttons3’>點擊按鈕3</button>
<script>
   var btn1 = document.getElementById(‘buttons1’);
   var btn2 = document.getElementById(‘buttons2’);
   var btn3 = document.getElementById(‘buttons3’);
</scrtip>
 
</body>

接下來定義setCommand函數,該函數負責往按鈕上面安裝命令。可以肯定的是,點擊按鈕會執行某個command命令,執行命令的動作被約定爲調用command對象的execute()方法。雖然還不知道這些命令究竟代表什麼操作,但負責繪製按鈕的程序員不關心這些事情,他們只需要預留好安裝命令的接口。

command對象自然知道如何和正確的對象溝通:

var setCommand = function(button,command){
  button.onclick = function(){
      command.execute();
   }
}

編寫具體功能的程序員交上他們的成果,他們完成了刷新菜單界面,增加子菜單和刪除子菜單這幾個功能,分別放在MenuBar和SubMenu這兩個對象中:

var MenuBar = {
  refresh:function(){
    console.log(‘刷新菜單目錄’);
  }
};
 
var SubMenu = {
 add:function(){
     console.log(‘增加子菜單’);
  }
 
 del:function(){
     console.log(‘刪除子菜單’);
  }
}

在讓button變得可用起來,我們需要把這些行爲都封裝在命令類中:

var RefreshMenuBarCommand= function(receiver){
  this.receiver = receiver;
}
 
RefreshMenuBarCommand.prototype.execute =function(){
   this.receiver.refresh();
}
 
var AddSubMenuCommand = function(receiver){
  this.receiver = receiver;
}
 
AddSubMenuCommand.prototype.execute = function(){
   this.receiver.add();
};
 
var DelSubMenuCommand = function(receiver){
  this.receiver = receiver;
}
DelSubMenuCommand.prototype.execute =function(){
  this.receiver.del();
}
 

最後就是把命令接收者傳入到command對象中,並且把command對象安裝到button上面:

 
var refreshMenucmd = new RefreshMenuBarCommand(MenuBar);
var addSubMenucmd = new AddSubMenuCommand(SubMenu);
var delSubMenucmd = new DelSubMenuCommand(SubMenu);
 
setCommand(btn1, refreshMenucmd);
setCommand(btn2, addSubMenucmd);
setCommand(btn3, delSubMenucmd);

9.3 javascript中的命令模式

也許我們會感到奇怪,所謂的命令模式,看起來就是給對象的某個方法取了個execute的名字。引入command對象和receiver這兩個無中生有的角色無非是把簡單的事情複雜化了,即使不用什麼模式,以下代碼也可以實現相同的功能

var  bindClick = function(button,func){
     button.onclick = func;
}
 
var MenuBar = {
refresh:function(){
    console.log(‘刷新菜單目錄’);
 }
};

var SubMenu = {
 add:function(){
     console.log(‘增加子菜單’);
  }
 
       del:function(){
      console.log(‘刪除子菜單’);
    }
}
bindClick(btn1,MenuBar.refresh);
bindClick(btn2,SubMenu.add);
bindClick(btn2,SubMenu.del);

以上說法是正確的,在上上節中我們是模擬傳統的面嚮對象語言的命令模式實現。命令模式將過程式的請求封裝在command對象的execute方法裏,能過封裝方法調用,我們可以把運算塊包裝成開。command對象可以被四處傳遞,所以在調用命令的時候,客戶Client不需要關心事情是如何進行的。

命令模式的由來,其實就是回調(callback)函數的一個面向對象的替代品

javascript作爲將函數作爲一等對象的語言,跟策略模式一樣的。早已融入到了javascript語言之中。運算塊不一定要封裝在command.execute方法中,也可以封閉在普通函數中。函數做爲一等對象,要本身就可以被四處傳遞。即使我們依然需要請求“接收者”,那也未必使用面向對象的方式,閉包可以完成同樣的功能。

在面向對象設計中,命令模式的接收者被當成command對象的屬性保存起來,同時約定執行命令的操作調用command.execute方法。在使用閉包的命令模式實現中,接收者被封閉在閉包產生的環境中,執行命令的操作可以更加簡單,僅僅執行回調函數即可。無論接收者被保存爲對象的屬性,還是被封閉在閉包產生的環境中,在將來執行命令的時候,接收者都能被順利訪問。用閉包實現如下:

var setCommand = function(button,func){
   button.onclick = function(){
       func();
   }
}
var MenuBar = {
  refresh:function(){
    console.log(‘刷新菜單界面’);
   }
};
 
var RefreshMenuBarCommand = function(receiver){
   return function(){
      receiver.refresh();
   }
}
 
var refreshMenuBarCommand =RefreshMenuBarCommand(menubar);
setCommand(btn1, refreshMenuBarCommand);
 

當然如果想更明確地表達當前正在使用的命令模式,或者除了執行命令之外、將來有可能還要提供撤銷命令等操作。我們最好還是把執行函數改爲調用execute方法:

var RefreshMenuBarCommand =function(receiver){
  return {
      execute:function(){
         receiver.refresh();
      }
  }
}
var setCommand = function(button,command){
   button.onclick =function(){
       command.execute();
   }
};
 
var refreshMenuBarCommand =RefreshMenuBarCommand(MenuBar);
setCommand(btn1, refreshMenuBarCommand);

9.4 撤銷命令

命令模式的作用不僅是封閉運算塊,而且可以很方便地給命令對象增加撤銷操作。就像訂餐時客人可以通過電話來取消訂單一樣。如下看個撤銷命令的例子:

我們在之前的講策略模式時講過一個Animate類來編寫一個動畫,這個動畫的可以讓頁面上的小球移動到水平方向的某個位置。現在在頁面上有一個input文本框和一個button按鈕,文本框中可以輸入一些數字,表示小球移動後的水平位置,小球在用戶點擊按鈕後立刻開始移動:

<body>
<div id="ball" style="position: absolute;background:#000;width: 50px;height: 50px"></div>
輸入小球移動後的位置:<input id="pos" type="text" />
<button id="moveBtn">開始移動</button>
 
<script type="text/javascript">
    var ball =document.getElementById('ball');
    var pos =document.getElementById('pos');
    var moveBtn =document.getElementById('moveBtn');
    moveBtn.onclick =function(){
        var animate = newAnimate(ball);
       animate.start('left',pos.value,1000,'strongEaseOut');
    }
</script>
</body>

如果文本框輸入200,然後點擊moveBtn按鈕,可以看到小球順利地移動到水平方向200px的位置。現在我們需要一個方法讓小球還原到開始移動之前的位置。當然也可以在文本框中輸入-200,並且點擊moveBtn按鈕,這也是個辦法,不過顯得很笨拙。頁面上最好有一個撤銷按鈕,點擊撤銷按鈕之後,小球便能回到上次的位置。

在給頁面中增加撤銷按鈕之前,先把目前的代碼改爲用命令模式實現。

var  ball =document.getElementById(‘ball’);
var  pos =document.getElementById(‘pos’);
var  moveBtn = document.getElementById(‘moveBtn’);
var  MoveCommand =function(receiver,pos){
     this.receiver = receiver;
     this.pos = pos;
}
 
 
var MoveComand.prototype.execute = function(){
     this.receiver.start(‘left’,this.pos,1000,’strongEaseOut’);
}
var  moveCommand;
moveBtn.onclick = function(){
    var  animate = new Animate(ball);
    moveCommand = new  MoveCommand(animate,pos.value);
    moveCommand.execute();
}

接下來增加撤銷按鈕:

<button  id=’cancelBtn’>cancel</button><!—增加取消按鈕-->

撤銷操作的實現一般是給命令對象增加一個名爲unexecude或者undo方法,在該方法裏執行execute的反向操作。在command.execute方法讓小球開始真正運動之前,我們需要先記錄小球的當前位置,在unexecute或者undo操作中,再讓小球回到剛剛記錄下的位置。代碼如下:

<script>
var  ball = document.getElementById(‘ball’);
var  pos = document.getElementById(‘pos’);
var  moveBtn = document.getElementById(‘moveBtn’);
var  MoveCommand = function(receiver,pos){
     this.receiver = receiver;
     this.pos = pos;
     this.oldPos = null;
}
 
 
var  MoveComand.prototype.execute = function(){
     this.receiver.start(‘left’,this.pos,1000,’strongEaseOut’);
     this.oldPos =this.receiver.dom.getBoundingClientRect()[this.receiver.propertyName];
}
 
 
MoveCommand.prototype.undo= function(){
    this.receiver.start(‘left’,this.oldPos,1000,’strongEaseOut’);
}
 
var  moveCommand;
moveBtn.onclick = function(){
    var  animate = new Animate(ball);
    moveCommand = new  MoveCommand(animate,pos.value);
    moveCommand.execute();
}
cancelBtn.onclick= function(){
    moveCommand.undo();
}
</script>


 

撤銷是命令模式裏一個非常有用的功能,試想一下開發一個圍棋程序的時候,我們把每一步棋子的變化都封閉成命令,則可以輕而易舉地實現悔棋功能。同樣,撤銷命令還可以用於實現文本編輯器的ctrl+z功能

9.5 撤銷和重做

上一節我們討論瞭如何撤銷一個命令。很多時候,我們需要撤銷一系列命令。比如在一個圍棋程序中,現在已經下了10步棋,我們需要一次性悔棋到第5步。在這之前,我們可以把所有執行過的下棋命令存儲在一個歷史列表中,然後倒序循環來依次執行這些命令的undo操作,直到循環執行到第5個命令爲止。然而,在某些情況下無法順利地用undo操作讓對象回到execute之前的狀態。比如在一個Canvas畫圖程序中,畫布上有一些點,我們在這些點之間畫了n條曲線把這些點相互連接起來,當然這是用命令模式來實現的。但是我們卻很難爲這些命令對象定義一個擦除某條曲線的undo操作,因爲在Canvas中,擦除一條線相對不容易實現。

這是時候最好的辦法是先清除畫布,然後把剛纔執行過的命令全部重新執行一遍這一點同樣可以利用一個歷史列表堆棧辦到。

作者編寫的《html5版街頭霸王》遊戲中,命令模式可以用來實現放錄像功能。原理跟Canvas畫圖的例子一樣,我們把用戶在鍵盤的輸入都封裝成命令,執行過的命令將被存放到堆棧中。播放錄像的時候只需要從頭開始依次執行這些命令便可:

<html>
  <body>
    <button  id= ‘replay’>播放錄像</button>
  </body>
  <script>
      var  Ryu = {
             attack:function(){
                console.log(‘攻擊’);
              },
              defense:function(){
                console.log(‘防禦’);
              },
              jump:function(){
                console.log(‘跳躍’);
              },
             crouch:function(){
                console.log(‘蹲下’);
              }
           };
      var  commands = {
               “119”:”jump”,//w
               “115”:”crouch”,//s
               “97”:”defense”,//a
               “100”:”attack”//d
 
            };
     var  commandStack = []; //保存命令的堆棧
     document.onkeypress = function(ev){
         var  keyCode = ev.keyCode;
         command =makeCommand(Ryu,commands[keyCode]);
        if(command){
           command(); //執行命令
          commandStack.push(command);
        }
     }
     document.getElementById(‘replay’).onclick= function(){
         var command;
         while(command=commandStack.shift()){
             command();
         }
     }
  </script>
</html>

可以看到,當我們在鍵盤上敲下W/A/S/D這幾個鍵來完成一些動作之後,再按下Replay按鈕,此時便會重複播放之前的動作。

9.6 命令隊列

在訂餐的故事中,如果訂單的數量過多而廚師的人手不夠,則可以讓這些訂單進行排隊處理。第一個訂單完成之後,再開始執行跟第二個訂單有關的操作。

隊列在動畫中的運用場景也非常多,比如之前的小球運動程序有可能遇到另外一個問題:有些用戶反饋,當快速連續點擊按鈕的,此時小球的前一個動畫可能尚未結束,於是前一個動畫會驟然停止,小球轉而開始第二個動畫的運動過程。但這不是用戶的期望,用戶希望這兩個動畫會排隊進行。

請示封裝成命令對象的優點在這裏再次體現出來,對象的生命週期幾乎是永久的,除非我們主動去回收它。也就是說,命令對象的生命週期跟初始請求發生的時間無關,command對象的execute方法可以在程序運行的任何時刻執行,即使點擊按鈕的請求早已發生,但我們的命令對象仍然是有生命的。

所以我們可以把div的這些運動過程都封裝成命令對象,再把它們壓進一個隊列堆棧,當動畫執行完,也就是當前command對象的職責完成之後,會主動通知隊列,此時取出正在隊列中等待的第一個命令對象,並且執行它。

我們比較關注的問題是,一個動畫結束後該如何通知隊列。通常可以使用回調函數來通知隊列,除了回調函數之外,還可以選擇發佈—訂閱模式。即在一個動畫結束後發佈一個消息,訂閱者接收到這個消息之後,便開始執行隊列的一下個動畫。

9.7 宏命令

宏命令是一組命令的集合,通過執行宏命令的方式,可能一次執行一批命令。想象一下,家裏有一個萬能遙控器,每天回家的時候,只要按一個特別的按鈕,它就會幫我們關上房間門,順便打開電腦並登錄QQ。

下面我們看看如何逐步創建一個宏命令。

首先,我們依然要創建好各種command:

var  closeDoorCommand = {
    execute:function(){
       console.log(‘關門’);
    }
};
var  openPcCommand = {
     execute:function(){
        console.log(‘開電腦’);
     }
};
var  openQQComand = {
     execute:function(){
        console.log(‘登錄QQ’);
     }
}

接下來定義宏命令的MacroCommand,它的結構也很簡單。macroCommand.add方法表示把子命令添加進宏命令對象,當調用宏命令對象的execute方法時,會迭代這一組命令對象,並且今次執行它的execute方法

var  MacroCommand =function(){
     return {
        commandsList:[],
        add:function(){
         this.command.push(command);
       },
       execute:function(){
          for(vari=0,command;command = this.commandsList[i++];){
            command.execute();
          }
       }
     }
};
var  macroCommand =MacroCommand();
macroCommand.add(closeDoorCommand);
macroCommand.add(openPcCommand);
macroCommand.add(openQQCommand);
macroCommand.execute();

當然我們還可以爲宏命令添加撤銷功能,跟macroCommand.execute類似,當調用macroCommand.undo方法時,宏命令裏包含的所有子命令對象要依次執行各自的undo操作。

9.8 智能命令與傻瓜命令

再看一下上節中創建的命令

var closeDoorCommand = {
 execute:function(){
    console.log(‘關門’);
 }
};

很奇怪,closeDoorCommand中沒有包含任何receiver的信息,它本身就包攬了執行請求的行爲,這跟我們之前看到的命令對象都包含一個receiver是矛盾的。

一般來說,命令模式都會在command對象中保存一個接收者負責真正執行客戶的請求,這種情況下的命令是“傻瓜式”的,它只負責把客戶的請求轉交給接收都來執行,這種模式的好處是請求發起者和請求接收者之間儘可能地得到解耦。

但是我們也可以定義一些更“聰明”的命令對象,“聰明”的命令對象可以直接實現請求,這樣一來就不在需要接收者的存在,這種“聰明”的命令對象也叫作智能命令。沒有接收者的智能命令,退貨到和策略模式非常相近,從代碼結構上已經無法分辨它們,能分辨的只有它們的意圖的不同。策略模式指向的問題域更小,所有策略對象的目標總是一致的,它們只是達到這個目標的不同手段,它們的內部實現是針對“算法”而言的。而智能命令模式指向的問題域更廣,command對象解決的目標更具發散性。命令模式還可以完成撤銷、排隊等功能。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章