浅谈javascript设计模式--命令模式

471 阅读7分钟

命令模式中一般包含三个角色: 命令调用者 -> 中间对象 -> 命令执行者

  1. 快到中午了,小明在公司点了一份蛋炒饭外卖,小明只需要通过电话向饭店下订单即可,无需知道是饭店的哪位厨师做这顿饭,此处小明是命令调用者
  2. 饭店前台收到蛋炒饭的订单后 告诉厨师做一份蛋炒饭,此处饭店前台是中间对象
  3. 前台告诉厨师有一个蛋炒饭的订单,厨师开始做蛋炒饭,无需知道是小明还是小红下的订单, 此处厨师相当于命令的执行者

为什么使用命令模式

命令的调用者命令的执行者 借助 命令中间对象 进行解耦,命令的调用者无需知道命令的执行者内部是如何处理的,只管调用相应的命令即可,命令的执行者也无需知道是谁调用的命令

命令模式常见的使用场景:

有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的具体操作是什么,此时希望用一种松耦合的方式来设计软件,使得请求发送者和请求接收者能够消除彼此之间的耦合关系。

案例1:使用命令模式设计一个菜单程序

假设现在有一个编写用户界面的程序,该用户页面上有数十个 button 按钮,因为项目比较大,开发主管决定让某个程序员同学绘制这些按钮,而另外一些程序员则负责编写点击按钮后的具体行为,这些行为都将被封装在对象里。对于绘制按钮的程序员来说,他完全不知道某个按钮未来用来做什么,他只知道点击这个按钮会发生某些事情。

针对这个需求来说,此处采用命令模式再适合不过了。

下面进入代码编写阶段,首先在页面中完成这些按钮的绘制:

<body>
  <div>
    <button id="btn1">刷新菜单</button>
    <button id="btn2">添加子菜单</button>
    <button id="btn3">删除子菜单</button>
  </div>
  <script>
    function getDomById(id) {
	    return document.getElementById(id);
    }

    // 命令调用者
    const btn1 = getDomById("btn1"), btn2 = getDomById('btn2'), btn3 = getDomById('btn3');
  </script>
</body>

接下来定义 setCommand 函数, setCommand 函数负责往按钮上安装命令。可以肯定的是,点击按钮会执行某个 command 命令,执行命令的动作被约定为调用 command 对象的 execute() 方法。虽然还不知道这些命令代表什么操作,但负责绘制按钮的程序员不关心这些事情,他只需要预留好安装这些命令的接口, command 对象自然知道如何和正确的对象沟通。

  function setComment(btn, comment) {
    btn.addEventListener('click', function () {
      comment.execute();
    })
  }

然后,负责编写点击按钮之后的具体行为的程序员总算交上了他们的成果,他们完成了刷新菜单界面,增加子菜单和删除子菜单这几个功能,这几个功能被分布在 MenuBarsubMenu 这两个对象中。

// 命令执行者
const MenuBar = {
	refresh: function () {
		console.log("刷新菜单目录");
	}
}

const SubMenu = {
	add: function () {
		console.log("增加子菜单");
	},
	del: function () {
		console.log("删除子菜单");
	}
}

在让 button 变得有用起来之前,我们要先把这些行为都封装在命令对象中。对命令对象的封装,我们可以借用js函数的闭包特性来实现。

  // 使用闭包实现命令中间对象的创建
  function RefreshMenuBarComment(receiver) {
    return {
      execute: function () {
        receiver.refresh();
      }
    }
  }

  function AddSubMenuComment(receiver) {
    return {
      execute: function () {
        receiver.add();
      }
    }
  }

  function DelSubMenuComment(receiver) {
    return {
      execute: function () {
        receiver.del();
      }
    }
  }

最后就是把命令接收者传入到 command 对象创建函数中,并把 command 对象安装到 button 上面。

  setComment(btn1, RefreshMenuBarComment(MenuBar));
  setComment(btn2, AddSubMenuComment(SubMenu));
  setComment(btn3, DelSubMenuComment(SubMenu));

以上只是一个简单的命令模式示例,从这个示例中,我们可以知道命令调用者和命令执行者是如何解耦开的。

命令模式的作用不仅仅是封装运算块,而且可以很方便的给命令对象增加撤销操作。就像订餐时可以通过电话来取消订单一样。

案例二:使用命令模式实现一个动画,这个动画让页面上的小球移动到水平方向的某个位置,也可以通过撤销按钮撤销小球上一次的移动。

现在页面上有一个input输入框和两个button按钮,文本框中可以输入一些数字,表示小球移动后的水平位置,小球在用户点击开始移动按钮后立刻开始移动,点击撤销移动按钮后可以撤销上一次的移动,回到移动前的位置。

先在页面上实现小球和按钮、输入框等元素

  <!DOCTYPE html>
  <html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>命令模式2</title>
    <style>
      html, body, div, button, input {
        margin: 0;
        padding: 0;
      }
      .ball {
        position: absolute;
        top: 50px;
        background: #000;
        width: 50px;
        height: 50px;
        border-radius: 50%;
      }
    </style>
  </head>
  <body>
    <div id="ball" class="ball"></div>
    输入小球移动后的位置:<input id="pos" />
    <button id="moveBtn">开始移动</button>
    <button id="undo">撤销移动</button>
  </body>
  </html>

接下来实现一个动画的构造函数

  var tween = {
    linear: function (t, b, c, d) {
      return c * t / d + b;
    },
    easeIn: function (t, b, c, d) {
      return c * (t /= d) * t + b;
    },
    strongEaseIn: function (t, b, c, d) {
      return c * (t /= d) * t * t * t * t + b;
    },
    strongEaseOut: function (t, b, c, d) {
      return c * ((t = t / d - 1) * t * t * t * t + 1) + b;
    },
    sineaseIn: function (t, b, c, d) {
      return c * (t /= d) * t * t + b;
    },
    sineaseOut: function (t, b, c, d) {
      return c * ((t = t / d - 1) * t * t + 1) + b;
    }
  }

  class Animate {
    constructor(dom) {
      this.dom = dom;
      this.startTime = 0;
      this.startPos = 0;
      this.endPos = 0;
      this.propertyName = null;
      this.easing = null;
      this.duretion = null;
    }

    start = (propertyName, endPos, duration, easing) => {
      this.startTime = +new Date();
      this.startPos = this.dom.getBoundingClientRect()[propertyName];
      this.propertyName = propertyName;
      this.endPos = endPos;
      this.duration = duration;
      this.easing = tween[easing];

      let self = this;
      let timeId = setInterval(function () {
        if (self.step() === false) {
          clearInterval(timeId);
        }
      }, 19)
    }

    step = () => {
      let t = +new Date();
      if (t > this.startTime + this.duration) {
        this.update(this.endPos);
        return false;
      }

      let pos = this.easing(t - this.startTime, this.startPos, this.endPos - this.startPos, this.duration);
      this.update(pos);
    }

    update = pos => {
      this.dom.style[this.propertyName] = pos + 'px';
    }
  }

接下来开始定义命令的调用者、命令的中间对象、命令的具体执行者

  <script>
    function getDomById(id) {
      return document.getElementById(id);
    }
    
    const ball = getDomById("ball"), pos = getDomById("pos");
    // 命令的调用者
    const  moveBtn = getDomById("moveBtn"), undo = getDomById("undo");
    let prePos; // 用来保存上一次小球移动的位置

    // 命令的具体执行者 或者称为命令的接收者
    const animate = new Animate(ball);

    // 创建命令中间对象
    const MoveCommand = function (receiver) {
      return {
        posStack: [],
        execute: function (pos) {
          receiver.start('left', pos, 1000, 'strongEaseOut');
          this.posStack.push(receiver.dom.getBoundingClientRect()[receiver.propertyName]);
        },
        unExecute: function () {
          if (this.posStack.length) {
            prePos = this.posStack.pop();
            receiver.start('left', prePos, 1000, 'strongEaseOut');
            pos.value = prePos;
          } else {
            console.log('已回退到最开始步骤');
          }
        },
      }
    }

    const moveCommand = MoveCommand(animate); // moveCommand 是创建的命令中间对象

    // 命令的调用者通过命令中间对象与命令的具体执行者进行解耦  

    moveBtn.addEventListener('click', function () {
      moveCommand.execute(pos.value);
    })

    undo.addEventListener('click', function () {
      moveCommand.unExecute();
    })
  </script>

小球在进行开始移动和撤销移动操作时,无需知道移动和撤销移动的具体行为操作,仅需借助命令中间对象调用相应的命令即可,从而实现命令的调用者 与命令的执行者解耦的目的。

以前看书,总是看过后当时记得,过了一段时间后就忘的差不多了,总结下原因,是因为看过之后没有及时做总结,这次阅读曾探大佬的《JavaScript设计模式与开发实践》,准备边看边做总结,既可以加深记忆与理解,也方便以后复习。

由于本人技术水平有限,部分内容可能描述的不清楚或存在错误,欢迎指出,感激不尽。

参考

《JavaScript设计模式与开发实践》 --曾探