JavaScript設(shè)計模式-命令模式

概念

??命令模式是最簡單和優(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ā)實踐》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末锹淌,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子赠制,更是在濱河造成了極大的恐慌赂摆,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異烟号,居然都是意外死亡绊谭,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門汪拥,熙熙樓的掌柜王于貴愁眉苦臉地迎上來达传,“玉大人,你說我怎么就攤上這事喷楣√舜螅” “怎么了?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵铣焊,是天一觀的道長逊朽。 經(jīng)常有香客問我,道長曲伊,這世上最難降的妖魔是什么叽讳? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮坟募,結(jié)果婚禮上岛蚤,老公的妹妹穿的比我還像新娘。我一直安慰自己懈糯,他們只是感情好涤妒,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著赚哗,像睡著了一般她紫。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上屿储,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天贿讹,我揣著相機(jī)與錄音,去河邊找鬼够掠。 笑死民褂,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的疯潭。 我是一名探鬼主播赊堪,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼竖哩!你這毒婦竟也來了踪少?” 一聲冷哼從身側(cè)響起恢着,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎公黑,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了舶衬。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡赎离,死狀恐怖逛犹,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情梁剔,我是刑警寧澤虽画,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站荣病,受9級特大地震影響码撰,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜个盆,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一脖岛、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧颊亮,春花似錦柴梆、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至雹有,卻和暖如春偿渡,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背件舵。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工卸察, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留脯厨,地道東北人铅祸。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像合武,于是被迫代替她去往敵國和親临梗。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容