概念
??命令模式是最簡單和優(yōu)雅的模式之一,命令模式中的命令(command)指的是一個執(zhí)行某些特定事情的指令熄浓。最常見的應(yīng)用場景是:有時候需要向某些對象發(fā)送請求糜芳,但是并不知道請求的接收者是誰鲁纠,也不知道被請求的操作是什么。此時希望用一種松耦合的方式來設(shè)計程序运提,使得請求發(fā)送者和請求接收者能夠消除彼此之間的耦合關(guān)系蝗柔。
描述
??拿訂餐來說,客人需要向廚師發(fā)送請求民泵,但是完全不知道這些廚師的名字和聯(lián)系方式癣丧,也不知道廚師炒菜的方式和步驟。命令模式把客人訂餐的請求封裝成command對象栈妆,也就是訂餐中的訂單對象坎缭。這個對象可以在程序中被四處傳遞,就像訂單可以從服務(wù)員手中傳到廚師的手中签钩。這樣一來掏呼,客人不需要知道廚師的名字,從而解開了請求調(diào)用者和請求接收者之間的耦合關(guān)系铅檩。
??另外憎夷,相對于過程化的請求調(diào)用,command對象擁有更長的生命周期昧旨。對象的生命周期是跟初始請求無關(guān)的拾给,因為這個請求已經(jīng)被封裝在了command對象的方法中,成為了這個對象的行為兔沃〗茫可以在程序運(yùn)行的任意時刻去調(diào)用這個方法,就像廚師可以在客人預(yù)定1個小時之后才幫他炒菜乒疏,相當(dāng)于程序在1個小時之后才開始執(zhí)行command對象的方法额衙。除了這兩點(diǎn)之外,命令模式還支持撤銷、排隊等操作窍侧。
應(yīng)用
菜單程序
??假設(shè)正在編寫一個用戶界面程序县踢,該用戶界面上至少有數(shù)十個Button按鈕。因為項目比較復(fù)雜伟件,所以決定讓某個程序員負(fù)責(zé)繪制這些按鈕硼啤,而另外一些程序員則負(fù)責(zé)編寫點(diǎn)擊按鈕后的具體行為,這些行為都將被封裝在對象里斧账。
??在大型項目開發(fā)中谴返,這是很正常的分工。對于繪制按鈕的程序員來說咧织,他完全不知道某個按鈕未來將用來做什么亏镰,可能用來刷新菜單界面,也可能用來增加一些子菜單拯爽,他只知道點(diǎn)擊這個按鈕會發(fā)生某些事情。那么當(dāng)完成這個按鈕的繪制之后钧忽,應(yīng)該如何給它綁定onclick事件呢毯炮?
??很快可以找到在這里運(yùn)用命令模式的理由:點(diǎn)擊了按鈕之后,必須向某些負(fù)責(zé)具體行為的對象發(fā)送請求耸黑,這些對象就是請求的接收者桃煎。但是目前并不知道接收者是什么對象,也不知道接收者究竟會做什么大刊。此時需要借助命令對象的幫助为迈,以便解開按鈕和負(fù)責(zé)具體行為對象之間的耦合。
??設(shè)計模式的主題總是把不變的事物和變化的事物分離開來缺菌,命令模式也不例外葫辐。按下按鈕之后會發(fā)生一些事情是不變的,而具體會發(fā)生什么事情是可變的伴郁。通過command對象的幫助耿战,將來可以輕易地改變這種關(guān)聯(lián),因此也可以在將來再次改變按鈕的行為焊傅。
??首先在頁面中完成這些按鈕的“繪制”:
<button id="button1">點(diǎn)擊按鈕1</button>
<button id="button2">點(diǎn)擊按鈕2</button>
<button id="button3">點(diǎn)擊按鈕3</button>
??接下來定義setCommand函數(shù)剂陡,setCommand函數(shù)負(fù)責(zé)往按鈕上面安裝命令『ィ可以肯定的是鸭栖,點(diǎn)擊按鈕會執(zhí)行某個command命令,執(zhí)行命令的動作被約定為調(diào)用command對象的execute()方法握巢。雖然還不知道這些命令究竟代表什么操作晕鹊,但負(fù)責(zé)繪制按鈕的程序員不關(guān)心這些事情,他只需要預(yù)留好安裝命令的接口,command對象自然知道如何和正確的對象溝通捏题。
let setCommand = function( button, command ){
button.onclick = function(){
command.execute();
}
}
??最后玻褪,負(fù)責(zé)編寫點(diǎn)擊按鈕之后的具體行為的程序員完成了刷新菜單界面、增加子菜單和刪除子菜單這幾個功能公荧,這幾個功能被分布在MenuBar和SubMenu這兩個對象中:
let MenuBar = {
refresh: function(){
console.log( '刷新菜單目錄' )
}
}
let SubMenu = {
add: function(){
console.log( '增加子菜單' )
},
del: function(){
console.log( '刪除子菜單' )
}
}
??在讓button變得有用起來之前带射,我們要先把這些行為都封裝在命令類中:
let RefreshMenuBarCommand = function( receiver ){
this.receiver = receiver
}
RefreshMenuBarCommand.prototype.execute = function(){
this.receiver.refresh()
}
let AddSubMenuCommand = function( receiver ){
this.receiver = receiver
}
AddSubMenuCommand.prototype.execute = function(){
this.receiver.add()
}
let DelSubMenuCommand = function( receiver ){
this.receiver = receiver
}
DelSubMenuCommand.prototype.execute = function(){
this.receiver.del()
}
??最后就是把命令接收者傳入到command對象中,并且把command對象安裝到button上面:
let refreshMenuBarCommand = new RefreshMenuBarCommand( MenuBar )
let addSubMenuCommand = new AddSubMenuCommand( SubMenu )
let delSubMenuCommand = new DelSubMenuCommand( SubMenu )
setCommand( button1, refreshMenuBarCommand )
setCommand( button2, addSubMenuCommand )
setCommand( button3, delSubMenuCommand )
撤銷和重做
??命令模式的作用不僅是封裝運(yùn)算塊循狰,而且可以很方便地給命令對象增加撤銷操作窟社。就像訂餐時客人可以通過電話來取消訂單一樣。
??撤銷操作的實現(xiàn)一般是給命令對象增加一個名為unexecute或者undo的方法绪钥,在該方法里執(zhí)行execute的反向操作灿里。在command.execute方法讓小球開始真正運(yùn)動之前,需要先記錄小球的當(dāng)前位置程腹,在unexecute或者undo操作中匣吊,再讓小球回到剛剛記錄下的位置。
??撤銷是命令模式里一個非常有用的功能寸潦,試想一下開發(fā)一個圍棋程序的時候色鸳,把每一步棋子的變化都封裝成命令,則可以輕而易舉地實現(xiàn)悔棋功能见转。同樣命雀,撤銷命令還可以用于實現(xiàn)文本編輯器的Ctrl+Z功能。
??很多時候斩箫,需要撤銷一系列的命令吏砂。比如在一個圍棋程序中,現(xiàn)在已經(jīng)下了10步棋乘客,需要一次性悔棋到第5步狐血。在這之前,可以把所有執(zhí)行過的下棋命令都儲存在一個歷史列表中易核,然后倒序循環(huán)來依次執(zhí)行這些命令的undo操作氛雪,直到循環(huán)執(zhí)行到第5個命令為止。
??然而耸成,在某些情況下無法順利地利用undo操作讓對象回到execute之前的狀態(tài)报亩。比如在一個Canvas畫圖的程序中,畫布上有一些點(diǎn)井氢,在這些點(diǎn)之間畫了N條曲線把這些點(diǎn)相互連接起來弦追,當(dāng)然這是用命令模式來實現(xiàn)的。但是卻很難為這里的命令對象定義一個擦除某條曲線的undo操作花竞,因為在Canvas畫圖中劲件,擦除一條線相對不容易實現(xiàn)掸哑。這時候最好的辦法是先清除畫布,然后把剛才執(zhí)行過的命令全部重新執(zhí)行一遍零远,這一點(diǎn)同樣可以利用一個歷史列表堆棧辦到苗分。記錄命令日志,然后重復(fù)執(zhí)行它們牵辣,這是逆轉(zhuǎn)不可逆命令的一個好辦法摔癣。
??在HTML5版的動作游戲中,命令模式可以用來實現(xiàn)播放錄像功能纬向。原理跟Canvas畫圖的例子一樣择浊,把用戶在鍵盤的輸入都封裝成命令,執(zhí)行過的命令將被存放到堆棧中逾条。播放錄像的時候只需要從頭開始依次執(zhí)行這些命令便可琢岩,代碼如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<button id="replay">播放錄像</button>
<script>
let Ryu = {
attack: function(){
console.log( '攻擊' )
},
defense: function(){
console.log( '防御' )
},
jump: function(){
console.log( '跳躍' )
},
crouch: function(){
console.log( '蹲下' )
}
}
let makeCommand = function( receiver, state ){ // 創(chuàng)建命令
return function(){
receiver[ state ]()
}
}
let commands = {
"119": "jump", // W
"115": "crouch", // S
"97": "defense", // A
"100": "attack" // D
}
let commandStack = [] // 保存命令的堆棧
document.onkeypress = function( ev ){
let keyCode = ev.keyCode,
command = makeCommand( Ryu, commands[ keyCode ] )
if ( command ){
command() // 執(zhí)行命令
commandStack.push( command ) // 將剛剛執(zhí)行過的命令保存進(jìn)堆棧
}
}
document.getElementById( 'replay' ).onclick = function(){ // 點(diǎn)擊播放錄像
let command
while( command = commandStack.shift() ){ // 從堆棧里依次取出命令并執(zhí)行
command()
}
}
</script>
</body>
</html>
小結(jié)
??命令模式的優(yōu)點(diǎn):
??第一,它能較容易地設(shè)計一個命令隊列师脂;第二担孔,在需要的情況下,可以較容易地將命令記入日志吃警;第三糕篇,允許接受請求的一方?jīng)Q定是否要否決請求;第四汤徽,可以容易地實現(xiàn)對請求的撤銷和重做;第五灸撰,由于加進(jìn)新的命令類不影響其他類谒府,因此增加新的具體命令類很容易;還有最關(guān)鍵的優(yōu)點(diǎn)浮毯,命令模式把請求一個操作的對象與知道怎么執(zhí)行一個操作的對象分割開完疫。
??那我們碰到類似情況就一定要實現(xiàn)命令模式嗎?敏捷開發(fā)原則告訴我們债蓝,不要為代碼添加給予猜測的壳鹤,實際不需要的功能。如果不清楚一個系統(tǒng)是否需要命令模式饰迹,一般就不要著急去實現(xiàn)它芳誓,事實上,在需要的時候通過重構(gòu)實現(xiàn)這個模式并不困難啊鸭。
參考文獻(xiàn)
《JavaScript設(shè)計模式與開發(fā)實踐》