假設(shè)有一個(gè)快餐店梨水,而我是該餐廳的點(diǎn)餐服務(wù)員,那么我一天的工作應(yīng)該是這樣的:當(dāng)某位客人點(diǎn)餐或者打來(lái)訂餐電話后茵臭,我會(huì)把他的需求都寫在清單上疫诽,然后交給廚房,客人不用關(guān)心是哪些廚師幫他炒菜。我們餐廳還可以滿足客人需要的定時(shí)服務(wù)踊沸,比如客人可能當(dāng)前正在回家的路上歇终,要求1個(gè)小時(shí)后才開始炒他的菜,只要訂單還在逼龟,廚師就不會(huì)忘記评凝。客人也可以很方便地打電話來(lái)撤銷訂單腺律。另外如果有太多的客人點(diǎn)餐奕短,廚房可以按照訂單的順序排隊(duì)炒菜。
這些記錄著訂餐信息的清單匀钧,便是命令模式中的命令對(duì)象翎碑。
命令模式的用途
命令模式是最簡(jiǎn)單和優(yōu)雅的模式之一,命令模式中的命令(command)指的是一個(gè)執(zhí)行某些特定事情的指令之斯。
命令模式最常見的應(yīng)用場(chǎng)景是:有時(shí)候需要向某些對(duì)象發(fā)送請(qǐng)求日杈,但是并不知道請(qǐng)求的接收者是誰(shuí),也不知道被請(qǐng)求的操作是什么佑刷。此時(shí)希望用一種松耦合的方式來(lái)設(shè)計(jì)程序莉擒,使得請(qǐng)求發(fā)送者和請(qǐng)求接收者能夠消除彼此之間的耦合關(guān)系。
拿訂餐來(lái)說(shuō)瘫絮,客人需要向廚師發(fā)送請(qǐng)求涨冀,但是完全不知道這些廚師的名字和聯(lián)系方式,也不知道廚師炒菜的方式和步驟麦萤。 命令模式把客人訂餐的請(qǐng)求封裝成command對(duì)象鹿鳖,也就是訂餐中的訂單對(duì)象。這個(gè)對(duì)象可以在程序中被四處傳遞翅帜,就像訂單可以從服務(wù)員手中傳到廚師的手中垛孔。這樣一來(lái),客人不需要知道廚師的名字狭莱,從而解開了請(qǐng)求調(diào)用者和請(qǐng)求接收者之間的耦合關(guān)系腋妙。
另外,相對(duì)于過(guò)程化的請(qǐng)求調(diào)用,command對(duì)象擁有更長(zhǎng)的生命周期袭景。對(duì)象的生命周期是跟初始請(qǐng)求無(wú)關(guān)的唁桩,因?yàn)檫@個(gè)請(qǐng)求已經(jīng)被封裝在了command對(duì)象的方法中,成為了這個(gè)對(duì)象的行為耸棒。我們可以在程序運(yùn)行的任意時(shí)刻去調(diào)用這個(gè)方法与殃,就像廚師可以在客人預(yù)定1個(gè)小時(shí)之后才幫他炒菜,相當(dāng)于程序在1個(gè)小時(shí)之后才開始執(zhí)行command對(duì)象的方法饥侵。
除了這兩點(diǎn)之外衣屏,命令模式還支持撤銷辩棒、排隊(duì)等操作一睁,稍后將會(huì)詳細(xì)講解。
命令模式的例子——菜單程序
假設(shè)我們正在編寫一個(gè)用戶界面程序窘俺,該用戶界面上至少有數(shù)十個(gè)Button按鈕复凳。因?yàn)轫?xiàng)目比較復(fù)雜,所以我們決定讓某個(gè)程序員負(fù)責(zé)繪制這些按鈕对途,而另外一些程序員則負(fù)責(zé)編寫點(diǎn)擊按鈕后的具體行為髓棋,這些行為都將被封裝在對(duì)象里惶洲。
在大型項(xiàng)目開發(fā)中膳犹,這是很正常的分工。對(duì)于繪制按鈕的程序員來(lái)說(shuō)币呵,他完全不知道某個(gè)按鈕未來(lái)將用來(lái)做什么侨颈,可能用來(lái)刷新菜單界面哈垢,也可能用來(lái)增加一些子菜單,他只知道點(diǎn)擊這個(gè)按鈕會(huì)發(fā)生某些事情举塔。那么當(dāng)完成這個(gè)按鈕的繪制之后求泰,應(yīng)該如何給它綁定onclick事件呢渴频?
回想一下命令模式的應(yīng)用場(chǎng)景:
有時(shí)候需要向某些對(duì)象發(fā)送請(qǐng)求,但是并不知道請(qǐng)求的接收者是誰(shuí)拔第,也不知道被請(qǐng)求的操作是什么蚊俺,此時(shí)希望用一種松耦合的方式來(lái)設(shè)計(jì)軟件逛万,使得請(qǐng)求發(fā)送者和請(qǐng)求接收者能夠消除彼此之間的耦合關(guān)系。
我們很快可以找到在這里運(yùn)用命令模式的理由:點(diǎn)擊了按鈕之后得封,必須向某些負(fù)責(zé)具體行為的對(duì)象發(fā)送請(qǐng)求呛每,這些對(duì)象就是請(qǐng)求的接收者坡氯。但是目前并不知道接收者是什么對(duì)象,也不知道接收者究竟會(huì)做什么手形。此時(shí)我們需要借助命令對(duì)象的幫助库糠,以便解開按鈕和負(fù)責(zé)具體行為對(duì)象之間的耦合。
設(shè)計(jì)模式的主題總是把不變的事物和變化的事物分離開來(lái)贷屎,命令模式也不例外艘虎。按下按鈕之后會(huì)發(fā)生一些事情是不變的野建,而具體會(huì)發(fā)生什么事情是可變的。通過(guò)command對(duì)象的幫助同眯,將來(lái)我們可以輕易地改變這種關(guān)聯(lián)须蜗,因此也可以在將來(lái)再次改變按鈕的行為肿孵。
下面進(jìn)入代碼編寫階段,首先在頁(yè)面中完成這些按鈕的“繪制”:
<body>
<button id="button1">點(diǎn)擊按鈕1</button>
<button id="button2">點(diǎn)擊按鈕2</button>
<button id="button3">點(diǎn)擊按鈕3</button>
</body>
<script>
var button1 = document.getElementById( 'button1' ),
var button2 = document.getElementById( 'button2' ),
var button3 = document.getElementById( 'button3' );
</script>
接下來(lái)定義setCommand函數(shù),setCommand函數(shù)負(fù)責(zé)往按鈕上面安裝命令大莫≈焕澹可以肯定的是,點(diǎn)擊按鈕會(huì)執(zhí)行某個(gè)command命令河咽,執(zhí)行命令的動(dòng)作被約定為調(diào)用command對(duì)象的execute()方法赋元。雖然還不知道這些命令究竟代表什么操作,但負(fù)責(zé)繪制按鈕的程序員不關(guān)心這些事情狠毯,他只需要預(yù)留好安裝命令的接口褥芒,command對(duì)象自然知道如何和正確的對(duì)象溝通:
var setCommand = function( button, command ){
button.onclick = function(){
command.execute();
}
};
最后锰扶,負(fù)責(zé)編寫點(diǎn)擊按鈕之后的具體行為的程序員總算交上了他們的成果,他們完成了刷新菜單界面罕偎、增加子菜單和刪除子菜單這幾個(gè)功能漓帅,這幾個(gè)功能被分布在MenuBar和SubMenu這兩個(gè)對(duì)象中:
var MenuBar = {
refresh: function(){
console.log( '刷新菜單目錄' );
}
};
var SubMenu = {
add: function(){
console.log( '增加子菜單' );
},
del: function(){
console.log( '刪除子菜單' );
}
};
在讓button變得有用起來(lái)之前忙干,我們要先把這些行為都封裝在命令類中:
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(){
console.log( '刪除子菜單' );
};
最后就是把命令接收者傳入到command對(duì)象中捐迫,并且把command對(duì)象安裝到button上面:
var refreshMenuBarCommand = new RefreshMenuBarCommand( MenuBar );
var addSubMenuCommand = new AddSubMenuCommand( SubMenu );
var delSubMenuCommand = new DelSubMenuCommand( SubMenu );
setCommand( button1, refreshMenuBarCommand );
setCommand( button2, addSubMenuCommand );
setCommand( button3, delSubMenuCommand );
以上只是一個(gè)很簡(jiǎn)單的命令模式示例施戴,但從中可以看到我們是如何把請(qǐng)求發(fā)送者和請(qǐng)求接收者解耦開的。
JavaScript中的命令模式
也許我們會(huì)感到很奇怪雷则,所謂的命令模式肪笋,看起來(lái)就是給對(duì)象的某個(gè)方法取了execute的名字藤乙。引入command對(duì)象和receiver這兩個(gè)無(wú)中生有的角色無(wú)非是把簡(jiǎn)單的事情復(fù)雜化了,即使不用什么模式而姐,用下面寥寥幾行代碼就可以實(shí)現(xiàn)相同的功能:
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( button1, MenuBar.refresh );
bindClick( button2, SubMenu.add );
bindClick( button3, SubMenu.del );
這種說(shuō)法是正確的拴念,之前的示例代碼是模擬傳統(tǒng)面向?qū)ο笳Z(yǔ)言的命令模式實(shí)現(xiàn)。命令模式將過(guò)程式的請(qǐng)求調(diào)用封裝在command對(duì)象的execute方法里划煮,通過(guò)封裝方法調(diào)用弛秋,我們可以把運(yùn)算塊包裝成形俐载。command對(duì)象可以被四處傳遞遏佣,所以在調(diào)用命令的時(shí)候,客戶(Client)不需要關(guān)心事情是如何進(jìn)行的意敛。
命令模式的由來(lái)膛虫,其實(shí)是回調(diào)(callback)函數(shù)的一個(gè)面向?qū)ο蟮奶娲贰?/p>
JavaScript作為將函數(shù)作為一等對(duì)象的語(yǔ)言稍刀,跟策略模式一樣,命令模式也早已融入到了JavaScript語(yǔ)言之中综膀。運(yùn)算塊不一定要封裝在command.execute方法中剧劝,也可以封裝在普通函數(shù)中抓歼。函數(shù)作為一等對(duì)象锭部,本身就可以被四處傳遞面褐。即使我們依然需要請(qǐng)求“接收者”展哭,那也未必使用面向?qū)ο蟮姆绞轿胖]包可以完成同樣的功能觉痛。
在面向?qū)ο笤O(shè)計(jì)中茵休,命令模式的接收者被當(dāng)成command對(duì)象的屬性保存起來(lái)榕莺,同時(shí)約定執(zhí)行命令的操作調(diào)用command.execute方法。在使用閉包的命令模式實(shí)現(xiàn)中吧史,接收者被封閉在閉包產(chǎn)生的環(huán)境中贸营,執(zhí)行命令的操作可以更加簡(jiǎn)單岩睁,僅僅執(zhí)行回調(diào)函數(shù)即可笙僚。無(wú)論接收者被保存為對(duì)象的屬性,還是被封閉在閉包產(chǎn)生的環(huán)境中亿笤,在將來(lái)執(zhí)行命令的時(shí)候净薛,接收者都能被順利訪問(wèn)蒲拉。用閉包實(shí)現(xiàn)的命令模式如下代碼所示:
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( button1, refreshMenuBarCommand );
當(dāng)然雌团,如果想更明確地表達(dá)當(dāng)前正在使用命令模式锦援,或者除了執(zhí)行命令之外,將來(lái)有可能還要提供撤銷命令等操作曼库。那我們最好還是把執(zhí)行函數(shù)改為調(diào)用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( button1, refreshMenuBarCommand );
撤銷命令
命令模式的作用不僅是封裝運(yùn)算塊毁枯,而且可以很方便地給命令對(duì)象增加撤銷操作。就像訂餐時(shí)客人可以通過(guò)電話來(lái)取消訂單一樣藐鹤。下面來(lái)看撤銷命令的例子教藻。
利用之前的Animate類來(lái)編寫一個(gè)動(dòng)畫右锨,這個(gè)動(dòng)畫的表現(xiàn)是讓頁(yè)面上的小球移動(dòng)到水平方向的某個(gè)位置∩芤疲現(xiàn)在頁(yè)面中有一個(gè)input文本框和一個(gè)button按鈕,文本框中可以輸入一些數(shù)字轧抗,表示小球移動(dòng)后的水平位置横媚,小球在用戶點(diǎn)擊按鈕后立刻開始移動(dòng)月趟,代碼如下:
<body>
<div id="ball" style="position:absolute;background:#000;width:50px;height:50px"></div>
輸入小球移動(dòng)后的位置:<input id="pos"/>
<button id="moveBtn">開始移動(dòng)</button>
</body>
<script>
var ball = document.getElementById( 'ball' );
var pos = document.getElementById( 'pos' );
var moveBtn = document.getElementById( 'moveBtn' );
moveBtn.onclick = function(){
var animate = new Animate( ball );
animate.start( 'left', pos.value, 1000, 'strongEaseOut' );
};
</script>
如果文本框輸入200孝宗,然后點(diǎn)擊moveBtn按鈕因妇,可以看到小球順利地移動(dòng)到水平方向200px的位置。現(xiàn)在我們需要一個(gè)方法讓小球還原到開始移動(dòng)之前的位置狡忙。當(dāng)然也可以在文本框中再次輸入-200去枷,并且點(diǎn)擊moveBtn按鈕,這也是一個(gè)辦法,不過(guò)顯得很笨拙逗余。頁(yè)面上最好有一個(gè)撤銷按鈕录粱,點(diǎn)擊撤銷按鈕之后画拾,小球便能回到上一次的位置青抛。
在給頁(yè)面中增加撤銷按鈕之前,先把目前的代碼改為用命令模式實(shí)現(xiàn):
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;
};
MoveCommand.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();
};
接下來(lái)增加撤銷按鈕:
<body>
<div id="ball" style="position:absolute;background:#000;width:50px;height:50px"></div>
輸入小球移動(dòng)后的位置:<input id="pos"/>
<button id="moveBtn">開始移動(dòng)</button>
<button id="cancelBtn">cancel</cancel> <!--增加取消按鈕-->
</body>
撤銷操作的實(shí)現(xiàn)一般是給命令對(duì)象增加一個(gè)名為unexecude或者undo的方法,在該方法里執(zhí)行execute的反向操作捣辆。在command.execute方法讓小球開始真正運(yùn)動(dòng)之前此迅,我們需要先記錄小球的當(dāng)前位置耸序,在unexecude或者undo操作中,再讓小球回到剛剛記錄下的位置坐昙,代碼如下:
var ball = document.getElementById( 'ball' );
var pos = document.getElementById( 'pos' );
var moveBtn = document.getElementById( 'moveBtn' );
var cancelBtn = document.getElementById( 'cancelBtn' );
var MoveCommand = function( receiver, pos ){
this.receiver = receiver;
this.pos = pos;
this.oldPos = null;
};
MoveCommand.prototype.execute = function(){
this.receiver.start( 'left', this.pos, 1000, 'strongEaseOut' );
this.oldPos = this.receiver.dom.getBoundingClientRect()[ this.receiver.propertyName ];
// 記錄小球開始移動(dòng)前的位置
};
MoveCommand.prototype.undo = function(){
this.receiver.start( 'left', this.oldPos, 1000, 'strongEaseOut' );
// 回到小球移動(dòng)前記錄的位置
};
var moveCommand;
moveBtn.onclick = function(){
var animate = new Animate( ball );
moveCommand = new MoveCommand( animate, pos.value );
moveCommand.execute();
};
cancelBtn.onclick = function(){
moveCommand.undo(); // 撤銷命令
};
現(xiàn)在通過(guò)命令模式輕松地實(shí)現(xiàn)了撤銷功能炸客。如果用普通的方法調(diào)用來(lái)實(shí)現(xiàn)痹仙,也許需要每次都手工記錄小球的運(yùn)動(dòng)軌跡开仰,才能讓它還原到之前的位置。而命令模式中小球的原始位置在小球開始移動(dòng)前已經(jīng)作為command對(duì)象的屬性被保存起來(lái)恩溅,所以只需要再提供一個(gè)undo方法脚乡,并且在undo方法中讓小球回到剛剛記錄的原始位置就可以了滨达。
撤銷是命令模式里一個(gè)非常有用的功能捡遍,試想一下開發(fā)一個(gè)圍棋程序的時(shí)候,我們把每一步棋子的變化都封裝成命令辆飘,則可以輕而易舉地實(shí)現(xiàn)悔棋功能劈猪。同樣战得,撤銷命令還可以用于實(shí)現(xiàn)文本編輯器的Ctrl+Z功能庸推。
撤消和重做
之前我們討論了如何撤銷一個(gè)命令贬媒。很多時(shí)候,我們需要撤銷一系列的命令坡倔。比如在一個(gè)圍棋程序中罪塔,現(xiàn)在已經(jīng)下了10步棋养葵,我們需要一次性悔棋到第5步关拒。在這之前,我們可以把所有執(zhí)行過(guò)的下棋命令都儲(chǔ)存在一個(gè)歷史列表中谐算,然后倒序循環(huán)來(lái)依次執(zhí)行這些命令的undo操作氯夷,直到循環(huán)執(zhí)行到第5個(gè)命令為止腮考。
然而玄捕,在某些情況下無(wú)法順利地利用undo操作讓對(duì)象回到execute之前的狀態(tài)枚粘。比如在一個(gè)Canvas畫圖的程序中馍迄,畫布上有一些點(diǎn),我們?cè)谶@些點(diǎn)之間畫了N條曲線把這些點(diǎn)相互連接起來(lái)暴凑,當(dāng)然這是用命令模式來(lái)實(shí)現(xiàn)的现喳。但是我們卻很難為這里的命令對(duì)象定義一個(gè)擦除某條曲線的undo操作嗦篱,因?yàn)樵贑anvas畫圖中幌缝,擦除一條線相對(duì)不容易實(shí)現(xiàn)涵卵。
這時(shí)候最好的辦法是先清除畫布缘厢,然后把剛才執(zhí)行過(guò)的命令全部重新執(zhí)行一遍,這一點(diǎn)同樣可以利用一個(gè)歷史列表堆棧辦到椿每。記錄命令日志间护,然后重復(fù)執(zhí)行它們汁尺,這是逆轉(zhuǎn)不可逆命令的一個(gè)好辦法。
在《街頭霸王》游戲中搂蜓,命令模式可以用來(lái)實(shí)現(xiàn)播放錄像功能帮碰。原理跟Canvas畫圖的例子一樣拾积,我們把用戶在鍵盤的輸入都封裝成命令拓巧,執(zhí)行過(guò)的命令將被存放到堆棧中肛度。播放錄像的時(shí)候只需要從頭開始依次執(zhí)行這些命令便可贤斜,代碼如下:
<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 makeCommand = function( receiver, state ){ // 創(chuàng)建命令
return function(){
receiver[ state ]();
}
};
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(); // 執(zhí)行命令
commandStack.push( command ); // 將剛剛執(zhí)行過(guò)的命令保存進(jìn)堆棧
}
};
document.getElementById( 'replay' ).onclick = function(){ // 點(diǎn)擊播放錄像
var command;
while( command = commandStack.shift() ){ // 從堆棧里依次取出命令并執(zhí)行
command();
}
};
</script>
</html>
可以看到瘩绒,當(dāng)我們?cè)阪I盤上敲下W锁荔、A蟀给、S、D這幾個(gè)鍵來(lái)完成一些動(dòng)作之后阳堕,再按下Replay按鈕跋理,此時(shí)便會(huì)重復(fù)播放之前的動(dòng)作。
命令隊(duì)列
在訂餐的故事中恬总,如果訂單的數(shù)量過(guò)多而廚師的人手不夠前普,則可以讓這些訂單進(jìn)行排隊(duì)處理。第一個(gè)訂單完成之后壹堰,再開始執(zhí)行跟第二個(gè)訂單有關(guān)的操作骡湖。
隊(duì)列在動(dòng)畫中的運(yùn)用場(chǎng)景也非常多,比如之前的小球運(yùn)動(dòng)程序有可能遇到另外一個(gè)問(wèn)題:有些用戶反饋峻厚,這個(gè)程序只適合于APM小于20的人群响蕴,大部分用戶都有快速連續(xù)點(diǎn)擊按鈕的習(xí)慣,當(dāng)用戶第二次點(diǎn)擊button的時(shí)候,此時(shí)小球的前一個(gè)動(dòng)畫可能尚未結(jié)束,于是前一個(gè)動(dòng)畫會(huì)驟然停止金刁,小球轉(zhuǎn)而開始第二個(gè)動(dòng)畫的運(yùn)動(dòng)過(guò)程。但這并不是用戶的期望劈狐,用戶希望這兩個(gè)動(dòng)畫會(huì)排隊(duì)進(jìn)行。
把請(qǐng)求封裝成命令對(duì)象的優(yōu)點(diǎn)在這里再次體現(xiàn)了出來(lái)呐馆,對(duì)象的生命周期幾乎是永久的懈息,除非我們主動(dòng)去回收它。也就是說(shuō)摹恰,命令對(duì)象的生命周期跟初始請(qǐng)求發(fā)生的時(shí)間無(wú)關(guān),command對(duì)象的execute方法可以在程序運(yùn)行的任何時(shí)刻執(zhí)行怒见,即使點(diǎn)擊按鈕的請(qǐng)求早已發(fā)生俗慈,但我們的命令對(duì)象仍然是有生命的。
所以我們可以把div的這些運(yùn)動(dòng)過(guò)程都封裝成命令對(duì)象遣耍,再把它們壓進(jìn)一個(gè)隊(duì)列堆棧闺阱,當(dāng)動(dòng)畫執(zhí)行完,也就是當(dāng)前command對(duì)象的職責(zé)完成之后舵变,會(huì)主動(dòng)通知隊(duì)列酣溃,此時(shí)取出正在隊(duì)列中等待的第一個(gè)命令對(duì)象,并且執(zhí)行它纪隙。
我們比較關(guān)注的問(wèn)題是赊豌,一個(gè)動(dòng)畫結(jié)束后該如何通知隊(duì)列。通趁嘣郏可以使用回調(diào)函數(shù)來(lái)通知隊(duì)列碘饼,除了回調(diào)函數(shù)之外,還可以選擇發(fā)布-訂閱模式悲伶。即在一個(gè)動(dòng)畫結(jié)束后發(fā)布一個(gè)消息艾恼,訂閱者接收到這個(gè)消息之后,便開始執(zhí)行隊(duì)列里的下一個(gè)動(dòng)畫麸锉。讀者可以嘗試按照這個(gè)思路來(lái)自行實(shí)現(xiàn)一個(gè)隊(duì)列動(dòng)畫钠绍。
宏命令
宏命令是一組命令的集合,通過(guò)執(zhí)行宏命令的方式花沉,可以一次執(zhí)行一批命令柳爽。想象一下媳握,家里有一個(gè)萬(wàn)能遙控器,每天回家的時(shí)候泻拦,只要按一個(gè)特別的按鈕毙芜,它就會(huì)幫我們關(guān)上房間門,順便打開電腦并登錄QQ争拐。
下面我們看看如何逐步創(chuàng)建一個(gè)宏命令腋粥。首先,我們依然要?jiǎng)?chuàng)建好各種Command:
var closeDoorCommand = {
execute: function(){
console.log( '關(guān)門' );
}
};
var openPcCommand = {
execute: function(){
console.log( '開電腦' );
}
};
var openQQCommand = {
execute: function(){
console.log( '登錄QQ' );
}
};
接下來(lái)定義宏命令MacroCommand架曹,它的結(jié)構(gòu)也很簡(jiǎn)單隘冲。macroCommand.add方法表示把子命令添加進(jìn)宏命令對(duì)象,當(dāng)調(diào)用宏命令對(duì)象的execute方法時(shí)绑雄,會(huì)迭代這一組子命令對(duì)象展辞,并且依次執(zhí)行它們的execute方法:
var MacroCommand = function(){
return {
commandsList: [],
add: function( command ){
this.commandsList.push( command );
},
execute: function(){
for ( var i = 0, command; command = this.commandsList[ i++ ]; ){
command.execute();
}
}
}
};
var macroCommand = MacroCommand();
macroCommand.add( closeDoorCommand );
macroCommand.add( openPcCommand );
macroCommand.add( openQQCommand );
macroCommand.execute();
當(dāng)然我們還可以為宏命令添加撤銷功能,跟macroCommand.execute類似万牺,當(dāng)調(diào)用macroCommand.undo方法時(shí)罗珍,宏命令里包含的所有子命令對(duì)象要依次執(zhí)行各自的undo操作。
宏命令是命令模式與組合模式的聯(lián)用產(chǎn)物脚粟。
智能命令與傻瓜命令
再看一下我們?cè)谥皠?chuàng)建的命令:
var closeDoorCommand = {
execute: function(){
console.log( '關(guān)門' );
}
};
很奇怪覆旱,closeDoorCommand中沒有包含任何receiver的信息,它本身就包攬了執(zhí)行請(qǐng)求的行為核无,這跟我們之前看到的命令對(duì)象都包含了一個(gè)receiver是矛盾的扣唱。
一般來(lái)說(shuō),命令模式都會(huì)在command對(duì)象中保存一個(gè)接收者來(lái)負(fù)責(zé)真正執(zhí)行客戶的請(qǐng)求团南,這種情況下命令對(duì)象是“傻瓜式”的噪沙,它只負(fù)責(zé)把客戶的請(qǐng)求轉(zhuǎn)交給接收者來(lái)執(zhí)行,這種模式的好處是請(qǐng)求發(fā)起者和請(qǐng)求接收者之間盡可能地得到了解耦吐根。
但是我們也可以定義一些更“聰明”的命令對(duì)象正歼,“聰明”的命令對(duì)象可以直接實(shí)現(xiàn)請(qǐng)求,這樣一來(lái)就不再需要接收者的存在佑惠,這種“聰明”的命令對(duì)象也叫作智能命令朋腋。沒有接收者的智能命令,退化到和策略模式非常相近膜楷,從代碼結(jié)構(gòu)上已經(jīng)無(wú)法分辨它們旭咽,能分辨的只有它們意圖的不同。策略模式指向的問(wèn)題域更小赌厅,所有策略對(duì)象的目標(biāo)總是一致的穷绵,它們只是達(dá)到這個(gè)目標(biāo)的不同手段,它們的內(nèi)部實(shí)現(xiàn)是針對(duì)“算法”而言的特愿。而智能命令模式指向的問(wèn)題域更廣仲墨,command對(duì)象解決的目標(biāo)更具發(fā)散性勾缭。命令模式還可以完成撤銷、排隊(duì)等功能目养。
小結(jié)
至此我們學(xué)習(xí)了命令模式俩由。跟許多其他語(yǔ)言不同,JavaScript可以用高階函數(shù)非常方便地實(shí)現(xiàn)命令模式癌蚁。命令模式在JavaScript語(yǔ)言中是一種隱形的模式幻梯。