• Javascript 实践中的命令模式


    定义

    Encapsulate a request as an object, thereby 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): 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()

    广州VI设计公司https://www.houdianzi.com

    总结

    通过以上几个例子,我们可以看出命令模式有一下几个特点:

    • 低耦合,彻底消除了接受者与调用者之间的耦合。
    • 易拓展,只需要增加新的命令便可拓展出新功能。
    • 支持序列化,易于实现保存与传递。
    • 容易导致 Command 类庞大。
  • 相关阅读:
    CoreSeek Sphinx 安装
    【Asp.net入门2-01】C#基本功能
    【Asp.net入门16】第一个ASP.NET 应用程序-总结
    【Asp.net入门15】第一个Asp.net应用程序-输入验证
    【Asp.net入门11】第一个ASP.NET 应用程序-创建摘要视图
    【Asp.net入门09】第一个ASP.NET 应用程序-处理窗体(1)
    【Asp.net入门08】第一个Asp.net应用程序-创建窗体并设置其样式
    【Asp.net入门07】第一个ASP.NET 应用程序-创建数据模型和存储库
    【Asp.net入门06】第一个ASP.NET 应用程序-案例说明
    【Asp.net入门05】第一个ASP.NET 应用程序-测试Asp.net程序
  • 原文地址:https://www.cnblogs.com/qianxiaox/p/14017458.html
Copyright © 2020-2023  润新知