Typescript 实践中的命令模式
前言
这篇文章是平时开发中对设计模式的体会与总结。
其中关于设计模式定义的描述主要来自 head first design pattern,UML 图来源于一个很优秀的 设计模式学习文档。
定义
Encapsulate a request as an object, thereby letting you parameterize other objects with different requests, queue or log requests,and support undoable operations. —
head first design pattern
「命令模式」将「请求」封装成对象,以便使用不同的请求、队列或者日志来参数化其他对象,同时支持可撤消的操作。
这里的「请求」的定义,并不是我们前端常说的「Ajax 请求」,而是一个「动作请求」,也就是发起一个行为。例如,通过遥控器关闭电视,这里的「关闭」就是一个请求。在命令模式中,我们将请求抽象成一个命令,这个命令是可复用的,它只关心它的接受者(电视);而对于动作的发起者(遥控器)来说,它只关心它所支持的命令有哪些,而不关心这些命令具体是做什么的。
结构
命令模式的类图如下:
在该类图中,我们看到五个角色:
- Client – 创建 Concrete Command 与 Receiver(应用层)。
- Invoker – 命令的发出者,可以持有很多的命令对象。
- Receiver – 命令接收者,真正被执行命令的对象。
- Command – 命令接口。
- ConcreteCommand – 命令接口的实现。
Reciver 与 Invoker 没有耦合,当需要拓展功能时,通过新增 Command,因此命令模式符合开闭原则。
实例
自定义快捷键
在一个文本编辑器中,通常支持自定义快捷键的功能,通过命令模式,我们可以写出一个将键位与键位逻辑解耦的结构。本例中我们将完成一个自定义复制、粘贴与剪切快捷键的功能。
interface Command { exec(): void } class CopyCommand implements Command { editor: Editor constructor(editor: Editor) { this.editor = editor } exec() { const { editor } = this editor.clipboard = editor.text.slice(...editor.range) } } class CutCommand implements Command { editor: Editor constructor(editor: Editor) { this.editor = editor } exec() { const { editor } = this editor.clipboard = editor.text.slice(...editor.range) editor.text = editor.text.slice(0, editor.range[0]) + editor.text.slice(editor.range[1]) } } class PasteCommand implements Command { editor: Editor constructor(editor: Editor) { this.editor = editor } exec() { const { editor } = this editor.text = editor.text.slice(0, editor.cursorIndex) + editor.clipboard + editor.text.slice(editor.cursorIndex) } } type Editor = { cursorIndex: number; range: [number, number]; text: string; clipboard: string; } const editor: Editor = { cursorIndex: 0, range: [0, 1], text: 'some text', clipboard: '' } type Keymap = { [key: string]: Command } class Hotkey { keymap: Keymap = {} constructor(keymap: Keymap) { this.keymap = keymap } call(e: KeyboardEvent) { const prefix = e.ctrlKey ? 'ctrl+' : '' const key = prefix + e.key this.dispatch(key) } dispatch(key: string) { this.keymap[key].exec() } } const keymap = { 'ctrl+x': new CutCommand(editor), 'ctrl+c': new CopyCommand(editor), 'ctrl+v': new PasteCommand(editor) } const hotkey = new Hotkey(keymap) document.onkeydown = (e) => { hotkey.call(e) }
在本例中,hotkey
是 Invoker,editor
是 Receiver。当我们需要修改已有的 keymap 时,只需要新增或替换已有的 key
或 Command
即可,例如将粘贴键位与剪切互换。
const keymap = { 'ctrl+x': new CopyCommand(editor), 'ctrl+c': new CutCommand(editor), 'ctrl+v': new PasteCommand(editor) }
是不是觉得这个写法似曾相识?没错 Redux 也是应用了命令模式,Store 相当于 Receiver,Action 相当于 Command,Dispatch 相当于 Invoker。
撤销与重做
基于命令模式,我们只需要一些简单的拓展,就能使它支持撤销功能。撤销之后再次执行 exec
就能实现重做。
interface Command { exec(): void undo(): void } class PasteCommand implements Command { editor: Editor prevText: '' // added constructor(editor: Editor) { this.editor = editor } exec() { const { editor } = this this.prevText = editor.text // added editor.text = editor.text.slice(0, editor.cursorIndex) + editor.clipboard + editor.text.slice(editor.cursorIndex) } undo() { this.editor.text = this.prevText // added } }
录制与回放
想想我们在游戏中的录制与回放功能,如果将角色的每个动作都作为一个命令的话,那么在录制时就能够得到一连串的命令队列。本例中,我们将记录 控制 ezio 的一系列命令。
interface IPerson { moveTo(x: number, y: number): void } class Person implements Person { x = 0 y = 0 moveTo(x: number, y: number) { this.x = x this.y = y } } interface Command { exec(): void } class MoveCommand implements Command { person: Person constructor(person: Person) { this.person = person } exec() { this.person.moveTo(this.person.x++, this.person.y++) } } class Control { commands: Command[] = [] exec(command) { this.commands.push(command) command.exec(this.person) } } const ezio = new Person() const control = new Control() control.exec(new MoveCommand(ezio)) control.exec(new MoveCommand(ezio)) console.log(control.commands)
当我们有了命令队列,我们又能够很容易得进行多次的撤销和重做,实现一个命令的历史记录。只需要移动当前命令队列的指针即可。
class CommandHistory { commands: Command[] = [] index = 0 get currentCommand() { return this.commands[index] } constructor(commands: Command[]) { this.commands = commands } redo() { this.index++ this.currentCommand.exec() } undo() { this.currentCommand.undo() this.index-- } }
同时,如果我们将命令序列化成一个对象,它便可以用于保存与传递。这样我们将它发送到远程计算机,就能实现远程控制 ezio
的功能。比如完成一系列唱跳 Rap 的行为。
['Move', 'Sing', 'Jump', 'Rap', 'Move']
宏命令
对 Command
进行一些简单的处理就能够将已有的命令组合起来执行,将其变成一个宏命令。
class BatchedCommand implements Command { commands = [] constructor(commands) { this.commands = commands } exec() { this.commands.forEach(command => command.exec()) } } const batchedMoveCommand = new BatchedCommand([ new MoveCommand(ezio), new SingCommand(ezio), new JumpCommand(ezio), new RapCommand(ezio), ]) batchedMoveCommand.exec()
总结
通过以上几个例子,我们可以看出命令模式有一下几个特点:
- 完全解耦,彻底消除了接受者与调用者之间的耦合。️
- 易拓展,只需要增加新的命令便可拓展出新功能。️
- 可序列化,易于实现保存与传递。️
- 太多的 Command 类容易导致系统臃肿。
原文地址:https://segmentfault.com/a/1190000021845532
相关推荐
-
Vue nextTick的源码理解,使用场景 javascript/jquery
2020-6-16
-
对JavaScript中的异步函数进行异常处理及测试 javascript/jquery
2019-1-25
-
AST in JS javascript/jquery
2019-1-1
-
深入理解 JavaScript中的变量、值、传参 javascript/jquery
2018-12-18
-
用不成熟的flutter web做了个网站 – 哭一场 javascript/jquery
2020-6-27
-
vue中track-by的理解 javascript/jquery
2020-6-9
-
为什么前后端分离了,你比从前更痛苦? javascript/jquery
2020-6-12
-
Ajax、Comet 与 Websocket javascript/jquery
2019-5-6
-
JavaScript的屏幕输出及时间函数 javascript/jquery
2019-10-9
-
Promise从两眼发懵到双眼放光(3)-Promise的几个关键问题(一) javascript/jquery
2020-5-28