本章研究的是一種封裝方法調用的方式。命令模式與普通函數有所不同。它可以用來對方法調用進行參數化處理和傳送,經過這樣處理過的方法調用可以在任何需要的時候執行。
它也可以用來消除調用操作的對象和實現操作的對象之間的耦合。這為各種具體的類的更換帶來了極大的靈活性。這種模式可以用在許多不同的場合,不過它在創建用戶界面這一方面非常有用,特別是在需要不受限的取消操作的時候。它還可以用來替代回調函數,因為它能夠提高在對象之間傳遞的操作的模塊化程度。
命令的結構:
- 最簡形式的命令對象是一個操作和用以調用這個操作的對象的結合體。
- 所有的命令對象都有一個執行操作。其用途就是調用命令對象所綁定的操作。
- 在大多數命令對象中,這個操作是一個名為execute或者run方法。
- 使用同樣接口的所有命令對象都可以被同等對待,並且互換。
假設:
- 你有一個廣告公司。想設計一個網頁。
- 客戶可以在上面執行一些與自己賬戶相關的操作,比如啟用或者停止某些廣告
- 因為不知具體廣告數量。你想設計一個盡可能靈活的UI
- 你打算用命令模式來弱化按鈕之類的用戶界面元素與其操作之間的耦合
var Interface = function () {} var adCommand = new Interface('adCommand',['execute']);
接下來需要定義倆個類,分別用來封裝廣告的start方法和stop方法
var StopAd = function (adObject) { this.ad = adObject; } StopAd.prototype.execute = function () { this.ad.stop(); } var StartAd = function (adObject) { this.ad = adObject; } StartAd.prototype.execute = function () { this.ad.start(); }
這是倆個非常典型的命令類,他們的構造函數以另一個對象為參數,而他們實現的execute方法則要調用該對象的某個方法。現在有了倆個可用在用戶界面中的類,他們具有相同的接口。你不知道也不關心adObject的具體實現細節,只要它實現了start和stop方法就行。借助於命令模式,可以實現用戶界面對象與廣告對象的隔離.
下面的代碼創建的用戶界面中,用戶名下的每個廣告都有倆個按鈕,分別用於啟動和停止廣告的輪播。
var ads = getAds(); for (var i = 0,len = ads.length; i < len; i++) { var startCommand = new StartAd(ads[i]); var stopCommand = new StopAd(ads[i]); new UiButton('Start' + ads[i].name, startCommand); new UiButton('Stop' + ads[i].name, stopCommand); }
UiButton 類的構造函數有倆個參數:一個是按鈕上的文字,另一個是命令對象。它會在網頁上生成一個按鈕,按鈕被點擊的時候會執行那個目錄對象都實現了excute方法,所以把任何一種命令對象提供給UiButtom,后者應該知道如何跟他打交道。這有助於創建高度模塊化和低耦合的用戶界面。
用閉包創建命令對象。
還有另外一種方法用來封裝函數,這種辦法不需要創建一個具有execute方法的對象。而是把想要的執行的方法包裝在閉包中。如果想要創建的目錄對象像前例中那樣只有一個方法,那么這種辦法尤其方便。現在你不再調用execute方法。因為那個命令可以作為函數直接執行,這樣還可以省卻作用域和this關鍵字綁定這方面的煩惱。
function makeStart(addObject) { return function () { addObject.start(); } } function makeStop(addObject) { return function () { addObject.stop(); } } var startCommand = makeStart(ads[0]); var stopCommand = makeStop(ads[0]); startCommand(); stopCommand();
這些命令函數可以像命令對象一樣四處傳遞,並且在需要的時候執行。他們是正式的命令對象類的簡單替代品,但是這個並不適於需要多個命令方法的場合,比如后面的那個實現取消功能的那個實例。
客戶、調用者和接收者。
到此你對命令模式已經有了一個大概了解。我們現在做點正式說明。這個系統中有三個參與者:客戶-client,調用者-invoking 和接收者-receiving。客戶負責實例化命令並將其交給調用者。在前面的例子中,for循環中的代碼就是客戶。它通常被包裝為一個對象,但是這不是必然的。調用者接過命令並將其保存下來。它會在某個時候調用該命令對象的execute方法,或者將其交給另一個潛在的調用者。前例中的調用者就是UiButton類創建的按鈕,調用者進行commandObject.execute這種調用時,它所調用的方法將轉而以receiver.action()這種形式調用恰當的方法。前例中的接收者就是廣告對象,它所執行的操作,要么就是start方法,要么就是stop方法。
什么參與者執行什么任務有時不太好記。這里再重復一遍:客戶創建命令;調用者執行該命令;接收者在命令執行時執行相應操作。除客戶端外的其它倆個參與者的名稱在一定程度上揭示了其作用,這有助於記憶。
所有使用命令模式的系統都有客戶和調用者,但不一定有接收者。有些復雜(但是模塊化程度較低)的命令並不調用接收者的方法,而是執行一些復雜查詢或命令,我們將在后面討論這種類型的命令。
在命令模式中使用接口
命令模式需要用到某種類型的接口,接口的作用在於確保接收者實現了所需要的操作,以及命令對象實現了正確的操作(他們有各種各樣的名稱,不過通常叫execute、run、undo)。不進行這種檢查的代碼比較脆弱,容易在運行期間出現難以排查的錯誤。你可以在自己的代碼中統一定義一個Command接口,但凡使用命令對象的地方,都驗檢查它是否實現了這個接口,這樣一來,其中所有命令對象的執行操作都具有相同的名稱,因此無需修改即可交換使用。這個接口大體形如:
var Command = new Interface('Command',['execute']);
有了這個接口,你就可以用類似於下面的代碼檢查命令對象是否實現了正確的執行操作:
Interface.ensureImplements(someCommand,Command);
someCommand.execute();
如果用閉包來創建命令函數,這種檢查更簡單,只需檢查該命令是否為函數即可。
if(typeof someCommand != 'function'){ throw new Error('Command is not a function'); }
所有類型的命令對象執行的都是同樣的任務:隔離調用操作的對象與實際實施操作的對象。這個定義所涵蓋的區間有倆種極端情況,前面創建的那種命令對象屬於區間一端,這種情況下的命令對象所起的作用只不過是吧現有接收者的操作(廣告對象的start和stop方法)與調用者按鈕綁定在一起。這類命令對象最簡單,其模塊化程度也最高,他們與客戶,接收者和調用者之間只是松散的耦合在一起。
var SimpleCommand = function (receving) { this.receving = receving; }; SimpleCommand.prototype.execute = function () { this.receving.action(); };
位於區間的另一端的則是那種封裝着一套復雜指令的命令對象,這種對象實際上沒有接收者,因為他們自己提供了操作的具體實現。它並不把操作委托給接收者實現,所有用於實現相關操作的代碼都包含在其內部:
var ComplexCommand = function () { this.logger = new Logger(); this.xhrHandler = XhrManager.createXhrHandler(); this.parameters = {}; } ComplexCommand.prototype = { setParameter: function (key,value) { this.parameters[key] = value; }, execute: function () { this.logger.log('Executing command'); var postArray = []; for(var key in this.parameters){ postArray.push(key + '=' + this.parameters[key]); } var postString = postArray.join('&'); this.xhrHandler.request( 'POST', 'script.php', function(){}, postString ); } };
這倆種極端之間存在一個灰色地帶。有些命令對象不但封裝了接收者的操作,而且其execute方法中也具有一些實現代碼,這類命令對象位於定義區間的中間地段:
var GeryAreaCommand = function (receiver) { this.logger = new Logger(); this.receiver = receiver; } GeryAreaCommand.prototype.execute = function () { this.logger.log('Executing command'); this.receiver.prepareAction(); this.receiver.action(); }
這些類型的命令對象各有各的用處,它們都能在項目中找到自己的位置。簡單命令對象一般用來消除倆個對象(接收者和調用者)之間的耦合,而復雜命令對象一般用來封裝不可分的或事務性的指令。
示例:菜單項
這個示例演示了如何用最簡單類型的命令對象構建模塊化的用戶界面。我們設計一個用來生成桌面應用程序風格的菜單欄的類,並通過使用命令對象,讓這些菜單執行各種操作。
借助於命令模式,我們可以把調用者(菜單項—)和接收者(實際執行操作的對象)隔離開。那些菜單項不必了解接收者的方法,它們只需要知道所有命令對象都實現了一個execute方法就行。這意味着同樣的命令對象也可以被工具欄圖標等其他用戶界面元素使用,而且並不需要修改。
這里沒有給出接收者類的實現代碼,其出發點在於你只需要知道接收者有些什么操作可供調用即可。
FileActions -open() -close() -save() -saveAS() EditActions -cut() -copy() -parste() -delete() InsertACtions -textBlock() HelpActions -showHelp
前面說過,接口在命令模式中起着非常重要的作用。這種作用在本例中尤其突出,這是因為我們還要為菜單使用組合模式,而組合對象又嚴重依賴接口,本例定義三個接口:
var Command = new Interface('Command',['execute']); var Composite = new Interface('Composite',['add','remove','getChild','getElement']); var MenuObject = new Interface('MenuObject',['show']);
1.菜單組合對象
接下來要實現的是 MenuBar,Menu,MenuItem 類。作為一個整體,他們要能顯示所有可用操作,並且根據要求調用這些操作。
MenuBar 和 Menu都是組合類對象,而MenuItem則是葉類,MenuBar類保存着所有Menu 實例:
var MenuBar = function () { this.menus = {}; this.element = document.createElement('ul'); this.element.style.display = 'none'; } MenuBar.prototype = { add: function (menuObject) { Interface.ensureImplements(menuObject, Composite, MenuObject); this.menus[menuObject.name] = menuObject; this.element.appendChild(this.menus[menuObject.name].getElement()); }, remove: function (name) { delete this.menus[name]; }, getChild: function (name) { return this.menus[name]; }, getElement: function () { return this.element; }, show: function () { this.element.style.display = "block"; for (var name in this.menus) { this.menus[name].show(); } } }
MenuBar 是一個很簡單的組合對象類。它會生成一個無序列表標簽,並且提供了想這個列表中添加菜單對象的方法。Menu類與此類似,不過它管理的是MenuItem實例:
var Menu = function (name) { this.name = name; this.items = {}; this.element = document.createElement('li'); this.element.innerHTML = this.name; this.element.style.display='none'; this.container = document.createElement('ul'); this.element.appendChild(this.container); } MenuBar.prototype = { add: function (menuObject) { Interface.ensureImplements(menuObject, Composite, MenuObject); this.items[menuObject.name] = menuObject; this.element.appendChild(this.items[menuObject.name].getElement()); }, remove: function (name) { delete this.items[name]; }, getChild: function (name) { return this.items[name]; }, getElement: function () { return this.element; }, show: function () { this.element.style.display = "block"; for (var name in this.items) { this.items[name].show(); } } }
值得一提的是,Menu類的item的屬性只起着一個查找表的作用,他不會保存菜單項的次序信息。菜單項的次序由DOM負責保持。每一條新添加的菜單項,都被添加在已有菜單項之后,如果要求能對菜單項的次序進行重排,那么可以把items屬性實現為數組。
真正讓人感興趣的是MenuItem類。這是系統中的調用者類。MenuItem的實例被用戶點擊時,會調用與其綁定在一起的命令對象。為此需要先確保傳入構造函數的命令對象實現了execute方法,然后再為MenuItem對象對應的錨標簽注冊click事件處理器中加入調用他們的語句。
var MenuItem = function (name,command) { Interface.ensureImplements(command, Command); this.name = name; this.element = document.createElement('li'); this.element.style.display = 'none'; this.auchor = document.createElement('a'); this.auchor.href = '#'; this.element.appendChild(this.auchor); this.auchor.innerHTML = this.name; addEvent(this.auchor,'click', function (e) { e.preventDefault(); command.execute(); }); } MenuItem.prototype = { add: function () { }, remove: function () { }, getChild: function () { }, getElement: function () { return this.element; }, show: function () { this.element.style.display = 'block'; } };
命令模式的作用在此開始顯現出來。你可以創建一個包含有許多菜單的非常復雜的菜單欄,而每個菜單欄都包含着一些菜單項。這些菜單項對如何執行自己所綁定的操作一無所知,它們也不需要知道哪些細節,它們唯一需要知道的就是命令對象有一個execute方法。
每個MenuItem都與一個命令對象綁定在一起。這個命令對象不能再改變,因為它被封裝在一個事件監聽器的閉包中。如果想改變菜單項所綁定的命令,必須另外創建一個新的MenuItem對象。
2.命令類:
MenuCommand這個命令類非常簡單。其構造函數的參數就是將被作為操作而調用的方法。因為javascript可以把對方法的引用作為參數傳遞 ,所以命令類只要把這個引用保存下來,然后在execute方法的執行過程中調用就可以了,這實際上是一個函數包裝對象。
var MenuCommand = function (action) { this.action = action; } MenuCommand.prototype.execute = function () { this.action(); };
如果action內部方法用到this關鍵字,那么它必須包裝在一個匿名函數中,如下所示:
ar someCommand = new MenuCommand(function () { myObject.someMethod(); })
匯合起來:
這個復雜結構的最終結果中的代碼實現很容易理解,其各個部分之間的耦合也比較松散,你需要做的就是創建MenuBar類的一個實例,然后為他添加一些Menu和MenuItem對象。其中每個MenuItem對象都綁定了一個命令對象。
var fileActions = new FileActions(); var EditActions = new EditActions(); var InsertACtions = new InsertACtions(); var HelpActions = new HelpActions(); var appMenuBar = new MenuBar(); //----------- var fileMenu = new Menu('File'); var openCommand = new MenuCommand(fileActions.open); var closeCommand = new MenuCommand(fileActions.close); var saveCommand = new MenuCommand(fileActions.save); var saveAsCommand = new MenuCommand(fileActions.saveAs); fileMenu.add(new MenuItem('open', openCommand)); fileMenu.add(new MenuItem('Close', closeCommand)); fileMenu.add(new MenuItem('Save', saveCommand)); fileMenu.add(new MenuItem('Close', saveAsCommand)); appMenuBar.add(fileMenu); //-------------- var editMenu = new Menu('Edit'); var cutCommand = new MenuCommand(EditActions.cut); var copyCommand = new MenuCommand(EditActions.copy); var pasteCommand = new MenuCommand(EditActions.paste); var deleteCommand = new MenuCommand(EditActions.delete); editMenu.add(new MenuItem('Cut', cutCommand)); editMenu.add(new MenuItem('Copy', copyCommand)); editMenu.add(new MenuItem('Paste', pasteCommand)); editMenu.add(new MenuItem('Delete', deleteCommand)); appMenuBar.add(editMenu); //------------ var insertMenu = new Menu('Insert'); var textBlockCommand = new MenuCommand(InsertACtions.textBlock); insertMenu.add(new MenuItem('Text Block', textBlockCommand)); appMenuBar.add(insertMenu); //------------ var helpMenu = new Menu('Help'); var showHelpCommand = new MenuCommand(HelpActions.showHelp()); helpMenu.add(new MenuItem('Show Help', showHelpCommand)); appMenuBar.add(helpMenu); document.getElementsByTagName('body')[0].appendChild(appMenuBar.getElement()); appMenuBar.show();
要是想為菜單再增添一些菜單項,很容易實現,例如下面倆行代碼就能在Insert菜單中添加一個圖像命令。
var imageCommand = new MenuCommand(InsertACtions.image); insertMenu.add(new MenuItem('Image', imageCommand));
這個菜單系統實現了接受用戶請求的對象與實現相關操作的對象的隔離。命令模式非常適合用來構建用戶界面,這是因為這種模式可以把執行具體工作的類與生成用戶界面的類隔離開來。在這種模式中,甚至可以讓多個用戶界面元素共用一個接收者或命令對象。既然命令可以作為一等對象進行傳遞和重用,那么它自然應該能夠反復執行。甚至被不同的調用者反復執行。
示例:取消操作和命令日志
還有一個方法也經常被實現為命令模式,那就是undo。借助這個方法,調用者可以回滾用execute執行的操作。undo方法可以用來實現不受限制的取消功能。只需要把執行過的命令對象壓入棧頂即可實現對命令執行歷史的記錄。如果用戶想撤銷最近的操作,他們可以點擊取消按鈕,這會從棧中彈出最近那個命令並調用該命令的undo方法。
下面模仿一個Etch A Sketch,游戲界面有四個按鈕,功能為把指針在上下左右四個方向移動10像素,此外還有一個取消按鈕,用來撤銷操作。首先,我們必須修改一下Command接口,為其添加一個undo方法。
var ReversibleCommand = new Interface('ReversibleCommand',['execute','undo']); //創建四個命令,分別向上下左右四個方向移動指針: var MoveUp = function (cursor) { this.cursor = cursor; } MoveUp.prototype = { execute: function () { this.cursor.move(0, -10); }, uodo: function () { this.cursor.move(0, 10); } } var MoveDowm = function (cursor) { this.cursor = cursor; } MoveDowm.prototype = { execute: function () { this.cursor.move(0, 10); }, uodo: function () { this.cursor.move(0, -10); } } var MoveLeft = function (cursor) { this.cursor = cursor; } MoveLeft.prototype = { execute: function () { this.cursor.move(-10, 0); }, uodo: function () { this.cursor.move(10, 0); } } var MoveRight = function (cursor) { this.cursor = cursor; } MoveRight.prototype = { execute: function () { this.cursor.move(10, 0); }, uodo: function () { this.cursor.move(-10, 0); } }
這些代碼很簡單。execute 方法向合適的方向移動指針,而undo方法則向相向的方向把指針移回去。最后,我們還需要有用作調用者的按鈕和實際負責實現指針移動的接收者。先來看看接收者:
var Cursor = function (width,height,parent) { this.width = width; this.height = height; this.position = { x:width/2, y:height/2 } this.canvas = document.createElement('canvas'); this.canvas.width = this.width; this.canvas.height = this.height; parent.appendChild(this.canvas); this.ctx = this.canvas.getContext('2d'); this.ctx.fillStyle = '#CCC000'; this.move(0, 0); } Cursor.prototype.move = function (x, y) { this.position.x +=x; this.position.y += y; this.ctx.clearRect(0, 0, this.width, this.height); this.ctx.fillRect(this.position.x, this.position.y, 3, 3); };
Cursor類實現了命令類所要求的操作。本例中涉及的操作只不過在特定位置繪制一個方塊。調用命令的是網頁上的按鈕。本例需要的按鈕有倆種,分別是用來調用execute方法的命令按鈕,和來來調用undo的取消按鈕。
在講按鈕之前,先來看看如何用裝飾者模式進一步提高命令類的模塊化程度。我們需要在系統中的某個地方加入一些用來把執行過的命令壓入棧的代碼。這些代碼可以寫在用戶界面類(按鈕類)中,這樣一來就必須在每一個用戶界面類中重復這些代碼,如果想把這些命令用於快捷鍵的話,還得再次實現這種入棧代碼。最好的辦法是用一個實現了這些代碼的裝飾者來包裝每一個命令。這樣我們就可以把命令對象傳遞給任意的用戶界面,不用擔心它是否實現了入棧代碼。
下面這個裝飾者的作用就是在執行一個命令之前先將其壓入棧。
var UndoDecorator = function (command,undoStack) { this.command = command; this.undoStack = undoStack; } UndoDecorator.prototype = { execute: function () { this.undoStack.push(this.command); this.command.execute(); }, undo: function () { this.command.undo(); } }
這是裝飾者模式出色的運用。藉此我們可以在保留原有接口的前提下為命令增添新的特性。在本例中這些裝飾者對象可以與所有命令對象互換使用。
現在來看看用戶界面類。這些負責生成必要的html元素,並且為其注冊click事件監聽器,這些監聽器要么調用execute方法要么調用 undo 方法:
var CommandButton = function (label,command,parent) { Interface.ensureImplements(command,ReversibleCommand); this.element = document.createElement('button'); this.element.innerHTML = label; parent.appendChild(this.element); addEvent(this.element,'click', function () { command.execute(); }); } var UndoButton = function (label,parent,undoStack) { this.element = document.createElement('button'); this.element.innerHTML = label; parent.appendChild(this.element); addEvent(this.element,'click', function () { if(undoStack.length === 0){ return ; } var lastCommand = undoStack.pop(); lastCommand.undo(); }); }
像UndoDecorator類一樣,UndoButton類的構造函數也需要把命令棧作為參數傳入。這個棧其實就是一個數組。調用經UndoDecorator對象裝飾過的命令對象的execute方法時這個命令對象會被壓入棧。為了執行取消操作,取消按鈕就會從命令棧中彈出最近的命令並調用其undo方法。這將逆轉剛執行的操作。
本例實例代碼也比較簡單,只需要實例化Cursor和所有命令,創建使用這些命令的按鈕,再創建一個空白棧即可:
var body = document.getElementsByTagName('body')[0]; var cursor = new Cursor(400, 400, body); var undoStack = []; var upCommand = new UndoDecorator(new MoveUp(cursor), undoStack); var downCommand = new UndoDecorator(new MoveDowm(cursor), undoStack); var leftCommand = new UndoDecorator(new MoveLeft(cursor), undoStack); var rightCommand = new UndoDecorator(new MoveRight(cursor), undoStack); var upButton = new CommandButton('Up', upCommand, body); var downButton = new CommandButton('Down', downCommand, body); var leftButton = new CommandButton('Left', leftCommand, body); var rightButton = new CommandButton('Right', rightCommand, body); var undoButton = new UndoButton('Undo', body, undoStack);
這段代碼生成的用戶界面包含一幅畫布 canvas 和 5個按鈕,點擊4個命令的任何一個按鈕都會向相應的方向移動指針,而點擊取消按鈕則會撤銷最后一次移動。
使用命令日志實現不可逆的操作取消:
有些取消是不可逆的,如果我們需要在指針后面留下一串尾跡,那么在畫布上畫線很容易,不過要取消這條線是不可能的,從A到B划一條線的逆轉並不是從B到A畫一條線。取消這種操作的唯一辦法就是清除狀態,然后把之前執行過的操作(不含最近那個)依次重做一遍。為此需要把執行命令都記錄在棧中,想取消一個操作,需要做的就是從棧中彈出最近那個命令丟棄,然后清理畫布並從頭開始重新執行記錄下來的所有命令。使用命令日志實現取消操作的系統不要求那些命令都是可逆命令,因此在本例中可以繼續使用原來的Command接口。
本例在原先的例子上修改很小,由於大多數代碼保持不動,所以我們只討論那些需要修改的地方。
第一個變化時我們刪除了所有命令對象的undo方法,原因是命令對象代表的操作現在不可逆。下面是其中一條命令類,其中undo方法已經刪除:
var MoveUp = function (cursor) { this.cursor = cursor; } MoveUp.prototype = { execute: function () { this.cursor.move(0, -10); } }
接下來,最大的變化發生在Cursor類代碼中。原來用來記錄命令的棧undoStack現在成了該類的內部屬性,名稱也改為commandStack。而UndoDecorator類和所有其他對undoStack的引用被刪除。新Cursor如下:
var Cursor = function (width,height,parent) { this.width = width; this.height = height; this.commandStack = []; this.canvas = document.createElement('canvas'); this.canvas.width = this.width; this.canvas.height = this.height; parent.appendChild(this.canvas); this.ctx = this.canvas.getContext('2d'); this.ctx.fillStyle = '#CCC000'; this.move(0, 0); } Cursor.prototype = { move: function (x,y) { var _this = this; this.commandStack.push(function () { _this.lineTo(x,y); }); }, lineTo: function (x,y) { this.position.x +=x; this.position.y +=y; this.ctx.lineTo(this.position.x, this.position.y); }, executeCommands: function () { this.position = {x: this.width / 2, y: this.height / 2}; this.ctx.clearRect(0, 0, this.width, this.height); this.ctx.beginPath(); this.ctx.moveTo(this.position.x, this.position.y); for (var i = 0; i < this.commandStack.length; i++) { this.commandStack[i](); } this.ctx.stroke(); }, undo: function () { this.commandStack.pop(); this.executeCommands(); } }
這里新增加了3個新方法,原有的move已被修改,它現在所做的就是把操作壓入命令棧。然后調用 executeCommands 方法。實際操作畫線的是lineTo方法。executeCommands負責重置canvas元素,然后依次執行命令棧中保存的操作。undo方法則會刪除最近的那條命令然后調用 executeCommands 重建系統狀態。
所有對undoStack的引用都被刪除,UndoButton中相關元素的click事件監聽器代碼也改了。
var undoButton = function (label,parent,cursor) { this.element = document.createElement('button'); this.element.innerHTML = label; parent.appendChild(this.element); addEvent(this.element,'click', function () { cursor.undo(); }); }
實現代碼與原來相同,唯一變化是刪除了undoStack,而傳給UndoButton 構造函數的參數也改成了一個Cursor實例。
var body = document.getElementsByTagName('body')[0]; var cursor = new Cursor(400, 400, body); var upCommand = new Moveup(cursor); var downCommand = new MoveDown(cursor); var leftCommand = new MoveLeft(cursor); var rightCommand = new MoveRight(cursor); var upButton = new CommandButton('Up', upCommand, body); var downButton = new CommandButton('Down', downCommand, body); var leftButton = new CommandButton('Left', leftCommand, body); var rightButton = new CommandButton('Right', rightCommand, body); var undoButton = new UndoButton('Undo', body, undoStack);
用於奔潰恢復的命令日志:
命令日志的一個有趣用途是在程序奔潰后恢復其狀態。在前面那個示例中,可以用XHR把經過序列號處理的命令記錄在服務器上。用戶下次訪問該網頁的時候,可以將畫布圖案精確還原。
命令模式的適用場合:
命令模式的主要用途是把調用對象(用戶界面,API和代理等)與實現操作的對象隔離開。照此而言,凡是倆個對象間的互動方式需要更高的模塊化程度的時候都可以用到這種模式。這是一種組織型模式,幾乎可以用在任何系統。不過,最能體現其效用的還是那種需要對操作進行規范化處理的場合。有了這種規范化處理,一個類或者一個調用者也能調用多種方法,而不需要事先為此了解哪些方法。
許多頁面元素非常符合這樣特征,比如前面菜單。命令模式可以徹底消除用戶界面元素與負責實際工作類之間的耦合。你可以為某個操作創建一個命令對象,然后用菜單項,工具圖標和鍵盤快捷鍵來調用這個對象。
可以受益於命令模式的還有其他一些特別場合。這種模式可以用來封裝XHR調用或其他延遲性調用場合的回調函數,用一個回調函數命令代替回調函數。可以把多條函數調用封裝為一個單位。有了命令對象的幫助,實現取消機制非常容易。實現不受限制的取消機制需要把執行過的命令保存在棧中。這種命令日志甚至可以用來取消本質上不可逆的操作,他還可以在任何應用程序奔潰之后用來恢復其整體狀態。
命令模式利弊:
好處:
- 如果運用得當,可以提高程序的模塊化程度和靈活性
- 實現取消和狀態恢復等復雜的有用特性非常容易
弊端:
- 用的勉強,容易浪費使用。如果你不需要模式給予的額外特性,也不需要接口一致,直接使用方法更恰當。
- 調試難度加大。總而言之,命令模式是一種用來封裝單個操作的結構型模式。
其封裝的操作可能是單個方法調用這么簡單,也可能是整個子程序那么復雜。經過封裝的對象可以作為一等的對象進行傳遞。命令對象主要用於消除調用者和接收者之間的耦合。這有助於創建高度模塊化的調用者,它們對所調用的操作不需要任何了解,這種模式也給程序員實現了接收者的自由,他們不必擔心接收者能否用在某套用戶界面中。這些負責的用戶特性用命令模式很容易實現,不受限制的取消和程序奔潰之后的狀態恢復就是這方面的倆個例子。它們還可以用來實現事務,具體做法就是把命令保存在棧中,隔段時間提交一次。
命令模式的最大優點在於,只要是能夠在execute方法中實現的操作,不管他有多復雜或者彼此間的差異多大,都能以與任何別的命令完全相同的方式進行傳送和調用。借助這種模式,代碼的重用幾乎可以達到一種不受限制的程度。