6_命令模式

1 命令模式的用途

  • 应用场景: 有时需要向某些对象发送请求, 但是并不知道请求的接收者是谁, 也不知道被请求的操作是什么

此时希望用一种松耦合的方式来设计程序,使得请求发送者和请求接收者能够消除彼此之间的耦合关系

订餐

  1. 命令模式将客人订餐的请求(订单)封装成 command 对象
  2. 订单从服务员手中传递给厨师

客人不需要知道厨师是谁 -- 解耦
客人可以预定订单 -- 异步

2 命令模式的例子 -- 菜单程序

2.1 前置条件

  1. A实现按钮批量绘制
  2. 其他人实现按钮点击的具体行为

思考:A在绘制完成之后,应该如何让B为按钮绑定onclick事件

2.2 实现

主题:将不变的事物和变化的事物分离开

按下按钮之后会发生一些事情是不变的,具体会发生什么事情是可变的

1. 绘制按钮
let btn1 = document.getElementById('button1')
let btn2 = document.getElementById('button2')
let btn3 = document.getElementById('button3')
2. 往按钮上安装命令 -- setCommand函数

约定:command.execute() -- 执行命令的动作

即使不知道这些命令具体代表什么操作,但只需要预留好安装命令的接口

function setCommand(btn, command) {
  btn.onclick = () => command.execute()
}
3. 定义具体行为
let MenuBar = {
  refresh() {
    console.log('刷新菜单目录')
  }
}
let SubMenu = {
  add() {
    console.log('增加子菜单')
  },
  del() {
    console.log('删除子菜单')
  }
}
4. 封装行为的命令类
class RefreshMenuBarCommand {
  constructor(receiver) {
    this.receiver = receiver
  }
  execute() {
    this.receiver.refresh()
  }
}
class AddSubMenuCommand {
  constructor(receiver) {
    this.receiver = receiver
  }
  execute() {
    this.receiver.add()
  }
}
class DelSubMenuCommand {
  constructor(receiver) {
    this.receiver = receiver
  }
  execute() {
    this.receiver.del()
  }
}
5. 将具体行为传入命令类
let refreshMenuBarCommand = new RefreshMenuBarCommand(MenuBar)
let addSubMenuCommand = new AddSubMenuCommand(SubMenu)
let delSubMenuCommand = new DelSubMenuCommand(SubMenu)
6. 将command对象安装到button上
setCommand(btn1, refreshMenuBarCommand)
setCommand(btn2, addSubMenuCommand)
setCommand(btn3, delSubMenuCommand)

3 js中的命令模式

3.1 简单实现绑定

function bindClick(btn, func) {
  btn.onclick = func
}
bindClick(btn1, MenuBar.refresh)
bindClick(btn2, SubMenu.add)
bindClick(btn3, SubMenu.del)

命令模式的接收者被当成 command 对象的属性保存起来,同时约定执行命令的操作调用 command.execute 方法
在使用闭包的命令模式实现中,接收者被封闭在闭包产生的环境中,执行命令的操作可以更加简单,仅仅执行回调函数即可

3.2 闭包实现命令模式

function setCommand(btn, func) {
  btn.onclick = () => {
    func()
  }
}
function RefreshMenuBarCommand(receiver) {
  return () => {
    receiver.refresh()
  }
}
let refreshMenuBarCommand = RefreshMenuBarCommand(MenuBar)
setCommand(btn1, refreshMenuBarCommand)
  • 除执行命令外,可能还有撤销命令等操作

3.3 闭包中调用 execute 方法

将执行函数改为调用 execute 方法

function setCommand(btn, command) {
  btn.onclick = () => {
    command.execute()
  }
}
function RefreshMenuBarCommand(receiver) {
  return {
    execute() {
      receiver.refresh()
    }
  }
}
let refreshMenuBarCommand = RefreshMenuBarCommand(MenuBar)
setCommand(btn1, refreshMenuBarCommand)

4 撤销命令

4.1 前置条件

input+btn 点击A按钮小球实现输入数值的水平位移,点击B按钮让小球位置复原

4.2 简单实现

let ball = document.getElementById('ball')
let pos = document.getElementById('pos')
let moveBtn = document.getElementById('moveBtn')
moveBtn.onclick = function() {
  let animate = new Animate(ball)
  animate.start('left', pos.value, 1000, 'strongEaseOut')
};

4.3 使用命令模式重写小球位移

class MoveCommand {
  constructor(receiver, pos) {
    this.receiver = receiver
    this.pos = pos
  }
  execute() {
    this.receiver.start(
      'left', this.pos, 1000, 'strongEaseOut'
    )
  }
}
let moveCommand
moveBtn.onclick = () => {
  let animate = new Animate(ball)
  moveCommand = new MoveCommand(animate, pos.value)
  moveCommand.execute()
}

4.4 点击B按钮让小球位置复原

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'
    )
  }
}
let moveCommand
let animate = new Animate(ball)
moveBtn.onclick = () => {
  moveCommand = new MoveCommand(animate, pos.value)
  moveCommand.execute()
}
cancelBtn.onclick = () => {
  console.log('moveCommand', moveCommand);
  moveCommand.undo()
}

5 撤消和重做

5.1 前置条件

  • 利用堆栈存储执行过的命令

播放游戏录屏功能实现

2. 实现命令播放

let Ryu = {
  attack() {
    console.log('攻击')
  },
  defense() {
    console.log('防御')
  },
  jump() {
    console.log('跳跃')
  },
  crouch() {
    console.log('蹲下')
  }
}
function makeCommand(receiver, state) {
  return () => {
    receiver[state]()
  }
}
let commands = {
  "119": "jump", // W
  "115": "crouch", // S
  "97": "defense", // A
  "100": "attack" // D
}

let commandStack = [] // 保存命令的堆栈
document.onkeypress = ev => {
  let keyCode = ev.keyCode,
    command = makeCommand(Ryu, commands[keyCode])
  if(command) {
    // 执行命令
    command() 
    // 记录执行过的命令
    commandStack.push(command) 
  }
}
document.getElementById('replay').onclick = () => {
  let command
  while(command = commandStack.shift()) {
    command()
  }
}

6 命令队列

  1. 等待当前命令执行时,将后续命令对象压入队列
  2. 当前 command 对象的职责完成后,主动通知队列,依次执行队列中的命令对象

7 宏命令

宏命令是一组命令的集合,通过执行宏命令的方式,可以一次执行一批命令

如何逐步创建一个宏命令

7.1 创建好各种 Command

let closeDoorCommand = {
  execute() {
    console.log('关门');
  }
}
let openPcCommand = {
  execute() {
    console.log('开电脑');
  }
}
let openQQCommand = {
  execute() {
    console.log('登录QQ');
  }
}

7.2 定义宏命令

class MacroCommand {
  constructor() {
    this.commandsList = []
  }
  add(command) {
    this.commandsList.push(command)
  }
  execute() {
    this.commandsList.map(command => command.execute())
  }
}

7.3 应用

let macroCommand = new MacroCommand()
macroCommand.add(closeDoorCommand)
macroCommand.add(openPcCommand)
macroCommand.add(openQQCommand)

macroCommand.execute()
posted on 2023-05-09 10:49  pleaseAnswer  阅读(127)  评论(0编辑  收藏  举报