javascript設計模式 之 6 命令模式

zhaoyezi發表於2018-06-01

1 命令模式的定義

命令模式:用於將一個請求封裝成為物件,從而使你可用不同的請求對客戶進行引數化;對請求排隊或者記錄請求日誌,以及執行可撤銷的操作。
也就是說:該模式旨在將函式的呼叫,請求和操作封裝成為一個單一的物件,然後對這個物件進行一系列的處理。 命令模式中的命令:指的是一個指向某些特定事情的指令。

2 現實中的命令模式

命令模式應用場景:有時向某些物件傳送請求,但是並不知道請求的接收者是誰,也不知道被請求的命令背後的操作是什麼此時希望一種鬆耦合的方式來設計軟體,使得請求傳送者和請求接收者能夠消除彼此之間的耦合關係。
。例如在一個快餐店,使用者向服務員點餐。服務員將使用者的需求記錄在清單上。

  • 請求者點菜:引數是菜名(我要什麼菜),時間(什麼時候要)。該需求封裝起來後,如果有變化我可以修改引數。
  • 命令模式將點餐內容封裝成為命令物件,命令物件就是填寫的清單。
  • 使用者不知道接收者(廚師)是誰,也不知道廚師的炒菜方式與步驟。
  • 請求者可以要求修改命令執行時間:例如晚1小時再要

3 選單例子

現在我們需要實現一個介面,包含很多個按鈕。每個按鈕有不同的功能,我們利用命令模式來完成。

  • 程式設計師A專門負責繪製按鈕[客戶點菜]
    通過setCommand函式,通過command建立命令。command就是服務員
<html>
    <body>
        <button id="MenuBar">MenuBar</button>
        <button id="SubMenu">SubMenu</button>
        <button id="subMenu2">subMenu2</button>
    </body>
    <script>
        var menuBarEl = document.getElementById( 'MenuBar');
        var subMenuEl = dxocument.getElementById( 'SubMenu');
        var subMenuEl2 = dxocument.getElementById( 'subMenu2');

        var setCommand = function(button, command) {
            button.addEventListener('click', function() {
                command.execute();
            });
        }
    </script>
</html>
複製程式碼
  • 程式設計師B,C 負責編寫每個按鈕的點選後具體發生的功能函式(廚師炒菜步驟)
// 使用物件導向的思想建立功能函式
var MenuBar= function(name) {
    this.name = name;
}
MenuBar.prototype.refresh = function() {
    console.log(this.name + '重新整理完成');
}


// 使用閉包的思想建立功能函式
var subMenu = function(name) {
    return {
        add: function() {
            console.log(name + '選單增加完畢');
        },
        del: function() {
            console.log(name + '選單刪除完畢');
        }
    }
}
複製程式碼
  • 程式設計師D封裝命令(服務員)
// 建立重新整理任務命令
var MenuBarCommand = function(receiver) {
    this.receiver = receiver;
}
MenuBarCommand.prototype.execute = function() {
    this.receiver.refresh();
}
// 建立新增選單命令
var subMenuAddCommand = function(reciever) {
        return {
            execute: function() {
                reciever.add();
            }
        }
    };

// 建立刪除選單命令
var subMenuDelCommand = function(reciever) {
        return {
            execute: function() {
                reciever.del();
            }
        }
    };
複製程式碼
  • 整個流程已經準備完畢,我們開始點餐觸發操作。
// 服務員需要知道廚師是誰
var menuBarCommand = new MenuBarCommand(new MenuBar('addMenu'));
// 點餐員告訴服務員點餐
setCommand(menuBarEl, menuBarCommand);

// 服務員需要知道廚師是誰(新增選單)
var subMenuCommand_add = subMenuAddCommand(subMenu('add_subMenu'));
// 點餐員告訴服務員點餐
setCommand(subMenuEl, subMenuCommand_add);

var subMenuCommand_del = subMenuDelCommand(subMenu('del_subMenu'));
setCommand(subMenuEl2, subMenuCommand_del);
複製程式碼
  • javascript中的命令模式 上面看起來,所謂命令模式就像是給物件的某個方法取了execute的名字。感覺command和receiver這兩個無中生有的角色把簡單的事情複雜化了。本來可以通過下面的方式就可以完成:
var bindClick = function(button, fn) {
    button.addEventListener('click', fn);
}

var subMenu =  {
    add: function(name) {
        console.log(name + '選單增加完畢');
    },
    del: function(name) {
        console.log(name + '選單刪除完畢');
    }

bindClick(subMenuEl, subMenu.add('subMenu'));
複製程式碼

這樣的說法時正確的,之前的例項是模擬傳統的面嚮物件語言的命令模式的實現,命令模式將過程式的請求封裝到command物件的execute()方法裡。通過封裝方法的呼叫,我們可以把運算塊包裝成形。command物件可以被四處傳遞。客戶端在呼叫命令的時候,不需要關心事情是如何進行的。

  • 命令模式的由來:其實是回撥(callback)函式的一個物件導向的替代品。
    javascript將函式作為一等物件的語言,跟策略模式一樣,命令模式已經早已融入到了javascript語言中。運算塊不一定需要封裝到command.execute()中,可也以封裝在普通函式中。函式作為一等物件,本省就可以四處傳遞。即使我們依然需要請求接收者,那也未必使用物件導向的方式,閉包也可以完成同樣的功能(上面的例子中subMenu使用的是閉包)。

4 撤銷命令

這裡我們還是對點餐的例子來實現。我們向餐廳定了一個盒飯,在6點的送來。

// 顧客點餐
var Customer = function(command) {
    return {
        book: function(food, time) {
            return command.execute(food, time);
        },
        undo: function(menu) {
            command.undo(menu);
        }
    };
}

// 服務員,擁有點餐方法和撤銷點餐方法
var foodCommand = function(cook){
    return {
        execute: function(food, time) {
            var timer = cook.willCook(food, time);
            return timer;
        }, 
        undo: function(food) {
            cook.unCook(food);
        }
    };
}

// 廚師
var cook = function() {
    return {
        willCook: function(food, time) {
            console.log('時間在' + time + ":開始煮:" + food);
            var timer = setTimeout(function() {
                console.log(food + '完成了');
            }, time);
            return timer;
        },
        unCook: function(timer) {
            clearTimeout(timer);
        }
    };
}

var command = foodCommand(cook());
var customer = Customer(command);
var receipt = customer.book('西紅寺炒雞蛋', 5000); // 5秒後炒完菜

customer.undo(receipt); //  做了取消操作,則不會炒菜
複製程式碼

5 命令佇列

在訂餐的故事中,如果訂單的數量過多而廚師的人手不夠,則應該讓這些訂單排隊處理,第一個訂單完成後,再完成第二個訂單。
請求封裝成為命令物件的有點:物件的生命週期幾乎是永久的,除非我們主動回收它。也就是說,命令物件的生命週期與初始化請求發生的時間無關,命令物件的execute方法可在程式原型的任何時刻執行。即使訂單的預定操作早就發生,但是我們的命令物件任然有生命。
我們可以把封裝的訂單命令壓入一個佇列堆疊,當前的訂單命令執行完畢,就主動通知佇列,此時去除正在佇列中等待的第一個命令物件並執行它。
我們應該如何在當前的訂單完成以後通知佇列,通常可以使用佇列函式,但是我們還可以選擇釋出-訂閱模式。即在一個訂單完成以後釋出一個訊息,訂閱者接收到這個訊息,開始執行下一個訂單內容。

  • 釋出者是廚師cook
  • 訂閱者是顧客
  • 中介是command

下面是錯誤的例子,我還不知道怎麼講釋出訂閱和命令模式結合在一起,誰看到了請指點一下:

var Customer = function(command) {
    return {
        book: function(food) {
            return command.execute(food);
        }
    };
}

var Command = function(cook) {
    return {
        execute: function(food) {
            cook.subscribe('cook', function() {
                console.log(food + 'complete');
            });
        }
    };
}

var Cook = function() {
    var cache = [];
    return {
        subscribe: function(food, fn) {
            if (!(food in cache)) {
                cache[food] = [];
            }
            cache[food].push(fn);
        },
        notify: function(food) {
            var fns = cache[food];
            if (!fns || fns.length === 0) {
                return;
            }
            fns.map(function(fn) {
                fn();
            });
        }
    };
}
var cook = Cook();
var customer = Customer(Command(cook));
customer.book('青菜炒茄子');
customer.book('黃瓜炒花椒');

cook.notify('cook');
複製程式碼

6 代理模式與命令模式區別

跟許多其他語言不同, JavaScript 可以用高階函式非常方便地實現命令模式。命令模式在 JavaScript 語言中是一種隱形的模式。

  • 在代理(委託)模式中,呼叫者就是委託者,執行者就是被委託者,委託者和被委託者介面定義是相同的;在命令模式中,呼叫者不關注執行者的介面定義是否和它一致。
  • 在呼叫時機上,代理模式的具體執行是隻能在特定的呼叫者內部執行(介面相同);命令模式的具體執行可以在任何呼叫者內部執行(介面不相同也可以)。

相關文章