Javascript 實踐中的命令模式

前言

這一系列是對平時工做與學習中應用到的設計模式的梳理與總結。
因爲關於設計模式的定義以及相關介紹的文章已經不少,因此不會過多的涉及。該系列主要內容是來源於實際場景的示例。
本篇文章爲該系列的第一篇,下一篇爲觀察者模式。前端

定義

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 請求」,而是一個「動做請求」,也就是發起一個行爲。例如,經過遙控器關閉電視,這裏的「關閉」就是一個請求。在命令模式中,咱們將請求抽象成一個命令,這個命令是可複用的,它只關心它的接受者(電視);而對於動做的發起者(遙控器)來講,它只關心它所支持的命令有哪些,而不關心這些命令具體是作什麼的。設計模式

結構

命令模式的類圖以下:編輯器

UML

在該類圖中,咱們看到五個角色:學習

  • Client - 建立 Concrete Command 與 Receiver(應用層)。
  • Invoker - 命令的發出者,一般會持有命令對象,能夠持有不少的命令對象。
  • Receiver - 命令接收者,真正執行命令的對象。任何類均可能成爲一個接收者,只要它可以實現命令要求實現的相應功能。
  • Command - 命令接口。
  • ConcreteCommand - 命令接口的實現。

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 時,只須要新增或替換已有的 keyCommand 便可。設計

是否是以爲這個寫法似曾相識?沒錯 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()

總結

經過以上幾個例子,咱們能夠看出命令模式有一下幾個特色:

  • 低耦合,完全消除了接受者與調用者之間的耦合。☑️
  • 易拓展,只須要增長新的命令即可拓展出新功能。☑️
  • 支持序列化,易於實現保存與傳遞。☑️
  • 容易致使 Command 類龐大。❌
相關文章
相關標籤/搜索