这一系列是对平时工做与学习中应用到的设计模式的梳理与总结。
因为关于设计模式的定义以及相关介绍的文章已经不少,因此不会过多的涉及。该系列主要内容是来源于实际场景的示例。
本篇文章为该系列的第一篇,下一篇为观察者模式。前端
Encapsulate a request as an object, thereby letting you parameterize other objects with different requests, queue or log requests,and support undoable operations.“
「命令模式」将「请求」封装成对象,以便使用不一样的请求、队列或者日志来参数化其余对象,同时支持可撤消的操做。typescript
这里的「请求」的定义,并非咱们前端常说的「Ajax 请求」,而是一个「动做请求」,也就是发起一个行为。例如,经过遥控器关闭电视,这里的「关闭」就是一个请求。在命令模式中,咱们将请求抽象成一个命令,这个命令是可复用的,它只关心它的接受者(电视);而对于动做的发起者(遥控器)来讲,它只关心它所支持的命令有哪些,而不关心这些命令具体是作什么的。设计模式
命令模式的类图以下:编辑器
在该类图中,咱们看到五个角色:学习
Reciver 与 Invoker 没有耦合,当须要拓展功能时,经过新增 Command,所以命令模式符合开闭原则。this
自定义快捷键是一个编辑器的最基本功能。经过命令模式,咱们能够写出一个将键位与键位逻辑解耦的结构。spa
interface Command { exec():void } 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() } } class CopyCommand implements Command { constructor(clipboard: any) {} exec() {} } class CutCommand implements Command { constructor(clipboard: any) {} exec() {} } class PasteCommand implements Command { constructor(clipboard: any) {} exec() {} } const clipboard = { data: '' } const keymap = { 'ctrl+x': new CutCommand(clipboard), 'ctrl+c': new CopyCommand(clipboard), 'ctrl+v': new PasteCommand(clipboard) } const hotkey = new Hotkey(keymap) document.onkeydown = (e) => { hotkey.call(e) }
在本例中,hotkey
是 Invoker,clipboard
是 Receiver。当咱们须要修改已有的 keymap 时,只须要新增或替换已有的 key
或 Command
便可。设计
是否是以为这个写法似曾相识?没错 Redux 也是应用了命令模式,Store 至关于 Receiver,Action 至关于 Command,Dispatch 至关于 Invoker。指针
基于命令模式,咱们能够很容易拓展,使它支持撤销与重作。日志
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 undo(): void } class MoveCommand implements Command { prevX = 0 prevY = 0 person: Person constructor(person: Person) { this.person = person } exec() { this.prevX = this.person.x this.prevY = this.person.y this.person.moveTo(this.prevX++, this.prevY++) } undo() { this.person.moveTo(this.prevX, this.prevY) } } const ezio = new Person() const moveCommand = new MoveCommand(ezio) moveCommand.exec() console.log(ezio.x, ezio.y) moveCommand.undo() console.log(ezio.x, ezio.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
移动的功能。
[{ type: 'move', x: 1, y: 1, }, { type: 'move', x: 2, y: 2, }]
对 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 SitCommand(ezio), ]) batchedMoveCommand.exec()
经过以上几个例子,咱们能够看出命令模式有一下几个特色: