面試官:說說你對命令模式的理解?應用場景?
本文轉載自微信公眾號「JS每日一題」,作者灰灰 。轉載本文請聯系JS每日一題公眾號。
一、是什么
命令模式是最簡單和優雅的模式之一,命令模式中的命令指的是一個執行某些特定事情的指令
該模式旨在將函數的調用、請求和操作封裝成為一個單一的對象
請求以命令的形式包裹在對象中,并傳給調用對象。調用對象尋找可以處理該命令的合適的對象,并把該命令傳給相應的對象,該對象執行命令
例如在一個快餐店,用戶向服務員點餐。服務員將用戶的需求記錄在清單上:
- 請求者點菜:參數是菜名(我要什么菜),時間(什么時候要),該需求封裝起來后,如果有變化我可以修改參數
- 命令模式將點餐內容封裝成為命令對象,命令對象就是填寫的清單
- 用戶不知道接收者(廚師)是誰,也不知道廚師的炒菜方式與步驟
- 請求者可以要求修改命令執行時間,例如晚1小時再要
二、實現
命令模式由三種角色構成:
- 發布者 invoker(發出命令,調用命令對象,不知道如何執行與誰執行)
- 接收者 receiver (提供對應接口處理請求,不知道誰發起請求)
- 命令對象 command(接收命令,調用接收者對應接口處理發布者的請求)

實現代碼如下:
- class Receiver { // 接收者類
- execute() {
- console.log('接收者執行請求');
- }
- }
- class Command { // 命令對象類
- constructor(receiver) {
- this.receiver = receiver;
- }
- execute () { // 調用接收者對應接口執行
- console.log('命令對象->接收者->對應接口執行');
- this.receiver.execute();
- }
- }
- class Invoker { // 發布者類
- constructor(command) {
- this.command = command;
- }
- invoke() { // 發布請求,調用命令對象
- console.log('發布者發布請求');
- this.command.execute();
- }
- }
- const warehouse = new Receiver(); // 廚師
- const order = new Command(warehouse); // 訂單
- const client = new Invoker(order); // 請求者
- client.invoke();
三、應用場景
命令模式最常見的應用場景是:有時候需要向某些對象發送請求,但是并不知道請求的接收者是誰,也不知道被請求的操作是什么。此時,希望用一種松耦合的方式來設計程序,使的請求發送者和請求接收者能夠消除彼此之間的耦合關系
菜單
現在我們需要實現一個界面,包含很多個按鈕。每個按鈕有不同的功能,我們利用命令模式來完成
- <button id="button1"></button>
- <button id="button2"></button>
- <button id="button3"></button>
- <script>
- var button1 = document.getElementById("button1");
- var button2 = document.getElementById("button2");
- var button3 = document.getElementById("button3");
- </script>
然后定義一個setCommand函數,負責將按鈕安裝命令,可以確定的是,點擊按鈕會執行某個 command 命令,執行命令的動作被約定為調用 command 對象的 execute() 方法。如下:
- var button1 = document.getElementById('button1')
- var setCommand = function(button, conmmand) {
- button.onclick = function() {
- conmmand.execute()
- }
- }
點擊按鈕之后具體行為包括刷新菜單界面、增加子菜單和刪除子菜單等,這幾個功能被分布在 MenuBar 和 SubMenu 這兩個對象中:
- var MenuBar = {
- refresh: function() {
- console.log('刷新菜單目錄')
- }
- }
- var SubMenu = {
- add: function() {
- console.log('增加子菜單')
- },
- del: function(){
- console.log('刪除子菜單');
- }
- }
這些功能需要封裝在對應的命令類中:
- // 刷新菜單目錄命令類
- class RefreshMenuBarCommand {
- constructor(receiver) {
- this.receiver = receiver;
- }
- execute() {
- this.receiver.refresh();
- }
- }
- // 增加子菜單命令類
- class AddSubMenuCommand {
- constructor(receiver) {
- this.receiver = receiver;
- }
- execute() {
- this.receiver.refresh();
- }
- }
- // '刪除子菜單命令類
- class DelSubMenuCommand {
- constructor(receiver) {
- this.receiver = receiver;
- }
- execute() {
- this.receiver.refresh();
- }
- }
最后就是把命令接收者傳入到 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);
撤銷
命令模式的作用不僅是封裝運算塊,而且可以很方便地給命令對象增加撤銷操作
頁面中有一個 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>
換成命令模式如下:
- 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();
- };
撤銷操作的實現一般是給命令對象增加一個名為 unexecude 或者 undo的方法,在該方法里執行 execute 的反向操作
在 command.execute 方法讓小球開始真正運動之前,需要先記錄小球的當前位置,在 unexecude 或者 undo 操作中,再讓小球回到剛剛記錄下的位置,代碼如下:
- class MoveCommand {
- constructor(receiver, pos) {
- this.receiver = receiver;
- this.pos = pos;
- this.oldPos = null;
- }
- execute() {
- this.receiver.start('left', this.pos, 1000, 'strongEaseOut');
- this.oldPos = this.receiver.dom.getBoundingClientRect()[this.receiver.propertyName]; // 記錄小球開始移動前的位置
- }
- undo() {
- 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方法中讓小球會到剛剛記錄的原始位置就可以
參考文獻
https://www.runoob.com/design-pattern/command-pattern.html
https://juejin.cn/post/6844903673697402888
https://juejin.cn/post/6995474681813811208