javascript設計模式——命令模式

来源:http://www.cnblogs.com/xiaohuochai/archive/2017/12/14/8034617.html
-Advertisement-
Play Games

[1]概念 [2]菜單程式 [3]命令模式 [4]撤銷和重做 [5]命令隊列 [6]巨集命令 ...


前面的話

  假設有一個快餐店,而我是該餐廳的點餐服務員,那麼我一天的工作應該是這樣的:當某位客人點餐或者打來訂餐電話後,我會把他的需求都寫在清單上,然後交給廚房,客人不用關心是哪些廚師幫他炒菜。餐廳還可以滿足客人需要的定時服務,比如客人可能當前正在回家的路上,要求1個小時後才開始炒他的菜,只要訂單還在,廚師就不會忘記。客人也可以很方便地打電話來撤銷訂單。另外如果有太多的客人點餐,廚房可以按照訂單的順序排隊炒菜。這些記錄著訂餐信息的清單,便是命令模式中的命令對象。本文將詳細介紹命令模式

 

概念

  命令模式是最簡單和優雅的模式之一,命令模式中的命令(command)指的是一個執行某些特定事情的指令。最常見的應用場景是:有時候需要向某些對象發送請求,但是並不知道請求的接收者是誰,也不知道被請求的操作是什麼。此時希望用一種松耦合的方式來設計程式,使得請求發送者和請求接收者能夠消除彼此之間的耦合關係

  拿訂餐來說,客人需要向廚師發送請求,但是完全不知道這些廚師的名字和聯繫方式,也不知道廚師炒菜的方式和步驟。命令模式把客人訂餐的請求封裝成command對象,也就是訂餐中的訂單對象。這個對象可以在程式中被四處傳遞,就像訂單可以從服務員手中傳到廚師的手中。這樣一來,客人不需要知道廚師的名字,從而解開了請求調用者和請求接收者之間的耦合關係

  另外,相對於過程化的請求調用,command對象擁有更長的生命周期。對象的生命周期是跟初始請求無關的,因為這個請求已經被封裝在了command對象的方法中,成為了這個對象的行為。可以在程式運行的任意時刻去調用這個方法,就像廚師可以在客人預定1個小時之後才幫他炒菜,相當於程式在1個小時之後才開始執行command對象的方法。除了這兩點之外,命令模式還支持撤銷、排隊等操作

 

菜單程式

  假設正在編寫一個用戶界面程式,該用戶界面上至少有數十個Button按鈕。因為項目比較複雜,所以決定讓某個程式員負責繪製這些按鈕,而另外一些程式員則負責編寫點擊按鈕後的具體行為,這些行為都將被封裝在對象里

  在大型項目開發中,這是很正常的分工。對於繪製按鈕的程式員來說,他完全不知道某個按鈕未來將用來做什麼,可能用來刷新菜單界面,也可能用來增加一些子菜單,他只知道點擊這個按鈕會發生某些事情。那麼當完成這個按鈕的繪製之後,應該如何給它綁定onclick事件呢?

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

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

  首先在頁面中完成這些按鈕的“繪製”

<button id="button1">點擊按鈕1</button>
<button id="button2">點擊按鈕2</button>
<button id="button3">點擊按鈕3</button>

  接下來定義setCommand函數,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 refreshMenuBarCommand = new RefreshMenuBarCommand( MenuBar );
var addSubMenuCommand = new AddSubMenuCommand( SubMenu );
var delSubMenuCommand = new DelSubMenuCommand( SubMenu );
setCommand( button1, refreshMenuBarCommand );
setCommand( button2, addSubMenuCommand );
setCommand( button3, delSubMenuCommand );

 

命令模式

  所謂的命令模式,看起來就是給對象的某個方法取了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( button1, MenuBar.refresh );
bindClick( button2, SubMenu.add );
bindClick( button3, SubMenu.del );

  命令模式的由來,其實是回調(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( button1, 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( button1, refreshMenuBarCommand );

 

撤銷和重做

  命令模式的作用不僅是封裝運算塊,而且可以很方便地給命令對象增加撤銷操作

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

<div id="ball" style="position:absolute;background:#000;width:50px;height:50px"></div>
輸入小球移動後的位置:<input id="pos"/>
<button id="moveBtn">開始移動</button>

<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,然後點擊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;
};
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();
};

  接下來增加撤銷按鈕:

<div id="ball" style="position:absolute;background:#000;width:50px;height:50px"></div>
輸入小球移動後的位置:<input id="pos"/>
<button id="moveBtn">開始移動</button>
<button id="cancelBtn">cancel</cancel> <!--增加取消按鈕-->

  撤銷操作的實現一般是給命令對象增加一個名為unexecute或者undo的方法,在該方法里執行execute的反向操作。在command.execute方法讓小球開始真正運動之前,需要先記錄小球的當前位置,在unexecute或者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 ];
            // 記錄小球開始移動前的位置
        };

        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(); // 撤銷命令
    };

  現在通過命令模式輕鬆地實現了撤銷功能。如果用普通的方法調用來實現,也許需要每次都手工記錄小球的運動軌跡,才能讓它還原到之前的位置。而命令模式中小球的原始位置在小球開始移動前已經作為command對象的屬性被保存起來,所以只需要再提供一個undo方法,並且在undo方法中讓小球回到剛剛記錄的原始位置就可以

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

  很多時候,需要撤銷一系列的命令。比如在一個圍棋程式中,現在已經下了10步棋,需要一次性悔棋到第5步。在這之前,可以把所有執行過的下棋命令都儲存在一個歷史列表中,然後倒序迴圈來依次執行這些命令的undo操作,直到迴圈執行到第5個命令為止

  然而,在某些情況下無法順利地利用undo操作讓對象回到execute之前的狀態。比如在一個Canvas畫圖的程式中,畫布上有一些點,在這些點之間畫了N條曲線把這些點相互連接起來,當然這是用命令模式來實現的。但是卻很難為這裡的命令對象定義一個擦除某條曲線的undo操作,因為在Canvas畫圖中,擦除一條線相對不容易實現

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

  在HTML5版的動作游戲中,命令模式可以用來實現播放錄像功能。原理跟Canvas畫圖的例子一樣,把用戶在鍵盤的輸入都封裝成命令,執行過的命令將被存放到堆棧中。播放錄像的時候只需要從頭開始依次執行這些命令便可,代碼如下:

<button id="replay">播放錄像</button>
<script>
    var Ryu = {
        attack: function(){
            console.log( '攻擊' );
        },
        defense: function(){
            console.log( '防禦' );
        },
        jump: function(){
            console.log( '跳躍' );
        },
        crouch: function(){
            console.log( '蹲下' );
        }
    };
    var makeCommand = function( receiver, state ){ // 創建命令
        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(); // 執行命令
            commandStack.push( command ); // 將剛剛執行過的命令保存進堆棧
        }
    };

    document.getElementById( 'replay' ).onclick = function(){ // 點擊播放錄像
        var command;
        while( command = commandStack.shift() ){ // 從堆棧里依次取出命令並執行
            command();
        }
    };
</script>

  在鍵盤上敲下W、A、S、D這幾個鍵來完成一些動作之後,再按下Replay按鈕,此時便會重覆播放之前的動作

 

命令隊列

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

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

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

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

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

 

巨集命令

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

  下麵逐步創建一個巨集命令。首先,創建好各種Command:

var closeDoorCommand = {
    execute: function(){
        console.log( '關門' );
    }
};
var openPcCommand = {
    execute: function(){
        console.log( '開電腦' );
    }
};

var openQQCommand = {
    execute: function(){
        console.log( '登錄QQ' );
    }
};

  接下來定義巨集命令MacroCommand,它的結構也很簡單。macroCommand.add方法表示把子命令添加進巨集命令對象,當調用巨集命令對象的execute方法時,會迭代這一組子命令對象,並且依次執行它們的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();

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

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 數組(Array)在JavaScript中是非常常用的類型,關於數組的排序,與C#等語法中的排序,乍看相似,其實差別比較大。 Array的排序方法有兩個,分別是reverse()和sort()。 reverse()方法:反轉數組項的順序,和sort()方法結合使用,可方便實現數組的升序和降序排列。 ...
  • 近段時間看了不少的前端面試題,很多大牛也對此做了整理。這篇文就算是借花獻佛啦。總體來說基本上每家問的問題也差不多,可能不同業務的公司問的問題的側重點也不太一樣,有的側重於移動端適配CSS佈局瀏覽器相容IE hack,而有的側重於JS邏輯面向對象設計模式考察等。可能將實際工作中會遇到的問題的場景,以及 ...
  • 1.內置對象 Date 日期對象 2.創建日期對象 2.1 根據當前的系統時間來創建日期對象。 2.2 根據指定的系統時間來創建日期對象。 參數有三種格式 2.3 獲取當前的時間,毫秒形式 2.4 獲取日期對象裡面的部分內容。 2.5 寫一個函數傳過來一個日期對象,返回yyyy MM dd HH:m ...
  • break 1.break 語句可用於跳出迴圈。 2.break所在的迴圈體已經結束。 continue 1.continue 語句中斷迴圈中的迭代,如果出現了指定的條件,然後繼續迴圈中的下一個迭代。 2.continue所在的迴圈體並沒有結束。 demo演示 ...
  • 1.switch case 一般的用它來做值匹配的。 //匹配 就是全等。 / 語法: switch(表達式){ case 值1: 表達式的值和 值1匹配上了,需要執行的代碼; break; case 值2: 表達式的值和 值2匹配上了,需要執行的代碼; break; case 值3: 表達式的值和 ...
  • 小伙伴們之前我們講過很多JavaScript的很多知識點,可以點擊回顧一下: 《JavaScript大廈之JS運算符》; 《JavaScript工作原理:記憶體管理 + 如何處理4個常見的記憶體泄露》; 《js 大廈之JavaScript事件》; 《JavaScript定義函數的幾種方式》; 《Java ...
  • 觀察者模式又叫做發佈訂閱模式,它定義了一種一對多的關係,讓多個觀察者對象同時監聽某一個主題對象,這個主題對象的狀態發生改變時就會通知所有觀察著對象。 它是由兩類對象組成,主題和觀察者,主題負責發佈事件,同時觀察者通過訂閱這些事件來觀察該主體,發佈者和訂閱者是完全解耦的,彼此不知道對方的存在,兩者僅僅 ...
  • 1.關係運算符 = ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...