美文网首页简友广场
JS设计模式之命令模式

JS设计模式之命令模式

作者: Splendid飞羽 | 来源:发表于2020-09-22 23:00 被阅读0次

    # 什么是“命令模式”?

    命令模式(别名:动作模式、事务模式)定义:将一个请求封装为一个对象,从而使我们可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。

    简单来说,它的核心思想是:不直接调用类的内部方法,而是通过给“指令函数”传递参数,由“指令函数”来调用类的内部方法。

    在这过程中,分别有 3 个不同的主体:调用者、传递者和执行者。
    请求以命令的形式包裹在对象中,并传给调用对象。调用对象寻找可以处理该命令的合适的对象,并把该命令传给相应的对象,该对象执行命令。

    # 应用场景

    当想降低调用者与执行者(类的内部方法)之间的耦合度时,可以使用此种设计模式。比如:设计一个命令队列,将命令调用记入日志。

    主要解决:在软件系统中,行为请求者与行为实现者通常是一种紧耦合的关系,但某些场合,比如需要对行为进行记录、撤销或重做、事务等处理时,这种无法抵御变化的紧耦合的设计就不太合适。

    命令模式的案例-菜单

    假设我们正在编写一个用户界面程序,该用户界面上至少有数十个Button按钮。因为项目比较复杂,所以我们决定让某个程序员负责绘制这些按钮,而另外一些程序员则负责编写点击按钮后的具体行为,这些行为都将被封装在对象里。在大型项目开发中,这是很正常的分工。对于绘制按钮的程序员来说,他完全不知道某个按钮未来将用来做什么,可能用来刷新菜单界面,也可能用来增加一些子菜单,他只知道点击这个按钮会发生某些事情。那么当完成这个按钮的绘制之后,应该如何给它绑定onclick事件呢?
    回想一下命令模式的应用场景:

    有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么,此时希望用一种松耦合的方式来设计软件,使得请求发送者和请求接收者能够消除彼此之间的耦合关系。

    我们很快可以找到在这里运用命令模式的理由:点击了按钮之后,必须向某些负责具体行为的对象发送请求,这些对象就是请求的接收者。但是目前并不知道接收者是什么对象,也不知道接收者究竟会做什么。此时我们需要借助命令对象的帮助,以便解开按钮和负责具体行为对象之间的耦合。

    设计模式的主题总是把不变的事物和变化的事物分离开来,命令模式也不例外。按下按钮之后会发生一些事情是不变的,而具体会发生什么事情是可变的。通过command对象的帮助,将来我们可以轻易地改变这种关联,因此也可以在将来再次改变按钮的行为。

    命令模式的由来,其实是回调(callback)函数的一个面向对象的替代品

    在面向对象设计中,命令模式的接收者被当成command对象的属性保存起来,同时约定执行命令的操作调用command.execute方法。在使用闭包的命令模式实现中,接收者被封闭在闭包产生的环境中,执行命令的操作可以更加简单,仅仅执行回调函数即可。无论接收者被保存为对象的属性,还是被封闭在闭包产生的环境中,在将来执行命令的时候,接收者都能被顺利访问。用闭包实现的命令模式如下代码所示:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>命令模式</title>
    </head>
    <body>
    <!--设置三个菜单按钮-->
    <button id="button1">点击按钮1</button>
    <button id="button2">点击按钮2</button>
    <button id="button3">点击按钮3</button>
    
    <script>
        var button1 = document.getElementById('button1');
        var button2 = document.getElementById('button2');
        var button3 = document.getElementById('button3');
        
         var setCommand = function (button, func) {
             button.onclick = function () {
                 func()
             }
         };
         var menuBar = {
             refresh: function () {
                 console.log('刷新菜单页面')
             },
             add: function () {
                 console.log('增加菜单页面')
             }
         };
         var RefreshMenuBarCommand = function (receiver) {
             return function () {
                 receiver.refresh()
             }
    
         };
         var AddMenuBarCommand = function (receiver) {
             return function () {
                 receiver.add()
             }
         };
        var refreshMenuBarCommand = RefreshMenuBarCommand(menuBar);
        var addMenuBarCommand = AddMenuBarCommand(menuBar);
        setCommand(button1, refreshMenuBarCommand);
        setCommand(button2, addMenuBarCommand)
    </script>
    </body>
    </html>
    

    当然,如果想更明确地表达当前正在使用命令模式,或者除了执行命令之外,将来有可能还要提供撤销命令等操作。那我们最好还是把执行函数改为调用execute方法:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>命令模式</title>
    </head>
    <body>
    <!--设置三个菜单按钮-->
    <button id="button1">点击按钮1</button>
    <button id="button2">点击按钮2</button>
    <button id="button3">点击按钮3</button>
    
    <script>
        var button1 = document.getElementById('button1');
        var button2 = document.getElementById('button2');
        var button3 = document.getElementById('button3');
        
         var setCommand = function (button, command) {
             button.onclick = function () {
                 // 通过command.execute调用
                 command.execute()
             }
         };
         var menuBar = {
             refresh: function () {
                 console.log('刷新菜单页面')
             },
             add: function () {
                 console.log('增加菜单页面')
             }
         };
         var RefreshMenuBarCommand = function (receiver) {
             return {
                 // 返回一个execute函数
                 execute: function () {
                     receiver.refresh()
                 }
             }
         };
         var AddMenuBarCommand = function (receiver) {
             return {
                 execute: function () {
                     receiver.refresh()
                 }
             }
         };
        var refreshMenuBarCommand = RefreshMenuBarCommand(menuBar);
        var addMenuBarCommand = AddMenuBarCommand(menuBar);
        setCommand(button1, refreshMenuBarCommand);
        setCommand(button2, addMenuBarCommand)
    </script>
    </body>
    </html>
    

    撤销命令

    命令模式的作用不仅是封装运算块,而且可以很方便地给命令对象增加撤销操作。记录上一次的操作,通过添加undo等方法回到上一步的状态

    撤销与重做
       很多时候,我们需要撤销一系列的命令。比如在一个围棋程序中,现在已经下了10步棋,我们需要一次性悔棋到第5步。在这之前,我们可以把所有执行过的下棋命令都储存在一个历史列表中,然后倒序循环来依次执行这些命令的undo操作,直到循环执行到第5个命令为止。

    然而,在某些情况下无法顺利地利用undo操作让对象回到execute之前的状态。比如在一个Canvas画图的程序中,画布上有一些点,我们在这些点之间画了N条曲线把这些点相互连接起来,当然这是用命令模式来实现的。但是我们却很难为这里的命令对象定义一个擦除某条曲线的undo操作,因为在Canvas画图中,擦除一条线相对不容易实现。

    这时候最好的办法是先清除画布,然后把刚才执行过的命令全部重新执行一遍,这一点同样可以利用一个历史列表堆栈办到。记录命令日志,然后重复执行它们,这是逆转不可逆命令的一个好办法。

    假如想要查看自己所释放过的技能,原理跟Canvas画图的例子一样,我们把用户在键盘的输入都封装成命令,执行过的命令将被存放到堆栈中。查看技能释放录像的时候只需要从头开始依次执行这些命令便可,代码如下:

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <title>重做</title>
    </head>
    
    <body>
        <button id="replay">播放录像</button>
    
        <script>
            var actions = {
                W: function() {
                    console.log('向前动作')
                },
                A: function() {
                    console.log('向左动作')
                },
                S: function() {
                    console.log('向后动作')
                },
                D: function() {
                    console.log('向右动作')
                }
            };
            var makeCommand = function(receiver, state) { // 创建命令
                return function() {
                    let func = receiver[state];
                    if (func instanceof Function)
                        func();
                }
            };
            var commands = {
                '119': 'W', // 前面的数字对应的ascii码的小写,后面的W指的是上面的技能函数
                '97': 'A',
                '115': 'S',
                '100': 'D',
            };
            var commandStack = []; // 保存命令的堆栈
            document.onkeypress = function(e) { // 用户按下键盘触发的事件
                var keyCode = e.keyCode,
                    command = makeCommand(actions, commands[keyCode]);
                if (command) {
                    command(); // 执行命令
                    commandStack.push(command); //将刚刚执行的命令放入到堆栈
                }
            };
            document.getElementById('replay').onclick = function() { // 点击播放录像
                console.log('-------开始播放动作录像--------')
                var command;
                while (command = commandStack.shift()) { // 从堆栈中取出命令依次执行
                    command();
                }
            }
        </script>
    </body>
    
    </html>
    

    当我们在键盘上敲下W、A、S、D这几个键来完成一些动作之后,再按下Replay按钮,此时便会重复播放之前的动作。
    项目中层遇到了这个问题,还有一个是撤销,即同样维护一个撤销的undoList,在每点击一个动作,将更新undoList,当点击撤销命令的时候,将undoList中的command取出执行即可。而且可以设置最长撤销或者重做的步数,超过步数不予处理,这样就可以通过命令模式维护了一个队列,经过项目的实战运用起来还是不错的。

    命令队列

    所以我们可以把div的这些运动过程都封装成命令对象,再把它们压进一个队列堆栈,当动画执行完,也就是当前command对象的职责完成之后,会主动通知队列,此时取出正在队列中等待的第一个命令对象,并且执行它。

    一个动画结束后该如何通知队列。通常可以使用回调函数来通知队列,除了回调函数之外,还可以选择发布-订阅模式。即在一个动画结束后发布一个消息,订阅者接=到这个消息之后,便开始执行队列里的下一个动画。读者可以尝试按照这个思路来自行实现一个队列动画。
    可以参考本系列文章之设计模式之组合模式。中的宏命令设置。

    相关文章

      网友评论

        本文标题:JS设计模式之命令模式

        本文链接:https://www.haomeiwen.com/subject/tftmyktx.html