這一系列是對平時工做與學習中應用到的設計模式的梳理與總結。
因爲關於設計模式的定義以及相關介紹的文章已經不少,因此不會過多的涉及。該系列主要內容是來源於實際場景的示例。
本篇文章爲該系列的第一篇,下一篇爲觀察者模式。前端
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()
經過以上幾個例子,咱們能夠看出命令模式有一下幾個特色: