hawtim.github.io icon indicating copy to clipboard operation
hawtim.github.io copied to clipboard

行为型模式-命令模式

Open hawtim opened this issue 5 years ago • 1 comments

命令模式是一种行为型模式。把对象之间的请求封装在命令对象中,将命令的调用者和命令的接收者完全解耦。

行为

当调用命令的 execute 方法时,不同的命令会做不同的事情,从而阐释不同的执行结果,此外还有 undo 和 redo 等操作。

电视遥控器例子

我们直接从代码入手,依次注释 命令的接收者、命令对象、命令的调用者

document.write(`<body>
<button id="execute">execute</button>
<button id="undo">undo</button>
</body>`)
// 命令的接收者
let Tv = {
  open() {
    console.log('open the tv')
  },
  close() {
    console.log('close the tv')
  }
}
// 命令对象
class OpenTvCommand {
  constructor(receiver) {
    this.receiver = receiver
  }
  // 行为 => 请求
  execute() {
    this.receiver.open()
  }
  undo() {
    this.receiver.undo()
  }
}
// 命令的调用者
let setCommand = (command) => {
  document.getElementById('execute').onclick = () => {
    command.execute()
  }
  document.getElementById('undo').onclick = () => {
    command.undo()
  }
}
// 实例化命令并解耦
setCommand(new OpenTvCommand(Tv))

上述的简单的例子可以直观的理解命令模式的原理。

我们接下看另外一个例子,通过控制小球的移动,本质的模式是相同的,只是增加了更多的逻辑,更接近我们的业务代码。

一个控制小球移动的例子

// 补充 Animate 类
let tween = {
  linear(t, b, c, d) {
    return c * t / d + b
  },
  easeIn(t, b, c, d) {
    return c * (t /= d) * t + b
  }
  // ...
}

class Animate {
  constructor(dom) {
    this.dom = dom // 进行运动的dom节点
    this.startTime = 0 // 动画开始时间
    this.endTime = 0
    this.startPos = 0 // 动画开始时,dom节点的位置,即dom的初始位置
    this.endPos = 0 // 动画开始时,dom节点的位置,即dom的目标位置
    this.propertyName = null // dom 节点需要被改变的 CSS 的属性名
    this.easing = null // 缓动算法
    this.duration = 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 timeId = setInterval(() => {
      if (this.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`
  }
}

document.write(`<body>
<div id="ball" style="position: absolute;top: 50px; left: 0; background: #000; width: 50px; height: 50px;"></div>
输入小球移动后的位置: <input id="pos"/>
<button id="moveBtn">开始移动</button>
<button id="cancelBtn">撤销移动</button>
</body>`)

let ball = document.getElementById('ball')
let pos = document.getElementById('pos')
let moveBtn = document.getElementById('moveBtn')
let cancelBtn = document.getElementById('cancelBtn')

// 控制小球的命令对象
class MoveCommand {
  constructor(receiver, pos) {
    this.receiver = receiver
    this.pos = pos
    this.oldPos = null
  }
  execute() {
    this.receiver.start('left', this.pos, 1000, 'linear')
    this.oldPos = this.receiver.dom.getBoundingClientRect()[this.receiver.propertyName]
  }
  undo() {
    this.receiver.start('left', this.oldPos, 1000, 'linear')
  }
}

var moveCommand

moveBtn.onclick = () => {
  var animate = new Animate(ball)
  moveCommand = new MoveCommand(animate, pos.value)
  moveCommand.execute()
}

cancelBtn.onclick = () => {
  moveCommand.undo()
}

优点

  • 降低程序的耦合度
  • 新的命令可以很容易的加入程序中
  • 可以设计一个命令队列和宏命令(组合命令)(参考组合命令)
  • 可以方便实现对请求的 undo 和 redo

缺点

  • 可能会导致程序中有过多的命令类

使用场景

  • 程序需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。
  • 程序需要在不同的时间指定请求、将请求排队和执行请求。
  • 程序需要支持命令的撤销(Undo)操作和恢复(Redo)操作。
  • 程序需要将一组操作组合在一起,即支持宏命令。

比如 编辑器的实现、命令行的实现等等。

总结

  • 命令模式是一种行为型模式,别名 动作模式 / 事务模式
  • 命令模式的本质是对命令进行封装,将发出命令的责任和执行命令的责任分割开。
  • 命令模式使请求本身成为一个对象,这个对象和其他对象一样可以被存储和传递。

hawtim avatar Oct 09 '20 13:10 hawtim

参考文档

命令模式

hawtim avatar Oct 09 '20 13:10 hawtim