Command
需要向某些对象发送请求:
- 解耦命令: 不清楚请求具体操作.
- 解耦接收者: 不清楚请求的接收者 (多个对象中的某个随机对象).
此时希望用一种松耦合的方式来设计程序, 使得请求发送者和请求接收者能够消除彼此的耦合关系:
- 将方法/动作封装成对象, 使得外部通过唯一方法
execute()/run()调用内部方法/动作. - 解耦三者: 客户创建命令, 调用者执行该命令, 接收者在命令执行时执行相应操作.
- 客户通常被包装为一个命令对象.
- 调用者接过命令并将其保存下来, 它会在某个时候调用该命令对象的
Command.execute()方法. - 调用者调用
Command.execute()后, 最终将调用接收者方法Receiver.action().
Command Use Case
- Decouple
ExecutorandReceiver. - GUI applications: bind
Commandto UI components. - Command sequences (store commands +
Compositepattern) for batch processing:Macro/Batch/Undo/Redofeature. - Command queue (cache commands +
Observerpattern) for transaction management:Redis/RabbitMQ/Kafka.
在 JS 中, Closure + Callback (Higher Order Function) 可以实现隐式的命令模式:
Closure捕获Receiver(面向对象语言中,Command对象需要持有Receiver对象).Callback函数实现具体逻辑 (面向对象语言中, 需要将其封装进Command.execute()对象方法).
// Higher order function
const Command = receiver => () => receiver.action()
Bind Command to UI components:
Executor: UI components.ClientandReceiver: background tasks or other UI components.Executor->ClientCommand.execute()->Receiver.action(): e.g. clickbutton-> refreshmenu.
// Executor
class Button {
commands = new Set()
add(command) {
this.commands.set(command)
}
click() {
for (const command of this.commands) command.execute()
}
}
// Client: command object, `action` implemented
class Command {
constructor(receiver) {
this.receiver = receiver
}
execute() {
this.receiver.action()
}
}
// Receiver
class MenuBar {
action() {
this.refresh()
}
refresh() {
console.log('refresh menu pages')
}
}
const button = new Button()
const menuBar = new MenuBar()
const refreshMenuBarCommand = new Command(menuBar)
button.add(refreshMenuBarCommand)
button.click()
class MenuCommand {
constructor(action) {
this.action = action
}
execute() {
this.action()
}
}
// --------------
const appMenuBar = new MenuBar()
// --------------
const fileActions = new FileActions()
const EditActions = new EditActions()
const InsertActions = new InsertActions()
const HelpActions = new HelpActions()
// --------------
const openCommand = new MenuCommand(fileActions.open)
const closeCommand = new MenuCommand(fileActions.close)
const saveCommand = new MenuCommand(fileActions.save)
const saveAsCommand = new MenuCommand(fileActions.saveAs)
const fileMenu = new Menu('File')
fileMenu.add(new MenuItem('open', openCommand))
fileMenu.add(new MenuItem('Close', closeCommand))
fileMenu.add(new MenuItem('Save', saveCommand))
fileMenu.add(new MenuItem('Close', saveAsCommand))
appMenuBar.add(fileMenu)
// --------------
const cutCommand = new MenuCommand(EditActions.cut)
const copyCommand = new MenuCommand(EditActions.copy)
const pasteCommand = new MenuCommand(EditActions.paste)
const deleteCommand = new MenuCommand(EditActions.delete)
const editMenu = new Menu('Edit')
editMenu.add(new MenuItem('Cut', cutCommand))
editMenu.add(new MenuItem('Copy', copyCommand))
editMenu.add(new MenuItem('Paste', pasteCommand))
editMenu.add(new MenuItem('Delete', deleteCommand))
appMenuBar.add(editMenu)
// --------------
const textBlockCommand = new MenuCommand(InsertActions.textBlock)
const insertMenu = new Menu('Insert')
insertMenu.add(new MenuItem('Text Block', textBlockCommand))
appMenuBar.add(insertMenu)
// --------------
const showHelpCommand = new MenuCommand(HelpActions.showHelp())
const helpMenu = new Menu('Help')
helpMenu.add(new MenuItem('Show Help', showHelpCommand))
appMenuBar.add(helpMenu)
// --------------
document.getElementsByTagName('body')[0].appendChild(appMenuBar.getElement())
appMenuBar.show()
Command sequences to implement Macro/Batch/Undo/Redo feature:
class Cursor {
constructor(width, height, parent) {
this.commandStack = []
this.width = width
this.height = height
this.canvas = document.createElement('canvas')
this.canvas.width = this.width
this.canvas.height = this.height
parent.appendChild(this.canvas)
this.ctx = this.canvas.getContext('2d')
this.ctx.fillStyle = '#CCC000'
this.move(0, 0)
}
move(x, y) {
this.commandStack.push(() => {
// `this` point to `Cursor`.
this.lineTo(x, y)
})
}
lineTo(x, y) {
this.position.x += x
this.position.y += y
this.ctx.lineTo(this.position.x, this.position.y)
}
executeCommands() {
this.position = { x: this.width / 2, y: this.height / 2 }
this.ctx.clearRect(0, 0, this.width, this.height)
this.ctx.beginPath()
this.ctx.moveTo(this.position.x, this.position.y)
for (let i = 0; i < this.commandStack.length; i++) this.commandStack[i]()
this.ctx.stroke()
}
undo() {
this.commandStack.pop()
this.executeCommands()
}
}