詳解Javascript實踐中的命令模式
定義
Encapsulate a request as an object,therebwww.cppcns.comy letting you parameterize other objects with different requests,queue or log requests,and support undoable operations.“
「命令模式」將「請求」封裝成物件,以便使用不同的請求、佇列或者日誌來引數化其他物件,同時支援可撤消的操作。
這裡的「請求」的定義,並不是我們前端常說的「Ajax 請求」,而是一個「動作請求」,也就是發起一個行為。例如,通過遙控器關閉電視,這裡的「關閉」就是一個請求。在命令模式中,我們將請求抽象成一個命令,這個命令是可複用的,它只關心它的接受者(電視);而對於動作的發起者(遙控器)來說,它只關心它所支援的命令有哪些,而不關心這些命令具體是做什麼的。
結構
命令模式的類圖如下:
在該類圖中,我們看到五個角色:
- Client - 建立 Concrete Command 與 Receiver(應用層)。
- Invoker - 命令的發出者,通常會持有命令物件,可以持有很多的命令物件。
- Receiver - 命令接收者,真正執行命令的物件。任何類都可能成為一個接收者,只要它能夠實現命令要求實現的相應功能。
- Command - 命令介面。
- ConcreteCommand - 命令介面的實現。
Reciver 與 Invoker 沒有耦合,當需要拓展功能時,通過新增 Command,因此命令模式符合開閉原則。
例項
自定義快捷鍵
自定義快捷鍵是一個編輯器的最基本功能。通過命令模式,我們可以寫出一個將鍵位與鍵位邏輯解耦的結構。
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): voidwww.cppcns.com } 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++) } unwww.cppcns.comdo() { 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.commanhttp://www.cppcns.comds.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()
總結
通過以上幾個例子,我們可以看出命令模式有一下幾個特點程式設計客棧:
- 低耦合,徹底消除了接受者與呼叫者之間的耦合。
- 易拓展,只需要增加新的命令便可拓展出新功能。
- 支援序列化,易於實現儲存與傳遞。
- 容易導致 Command 類龐大。
以上就是詳解javascript實踐中的命令模式的詳細內容,更多關於javascript命令模式的資料請關注我們其它相關文章!