设计模式之-命令模式

1 .命令模式的用途

命令模式是最简单和最优雅的模式之一,命令模式中的命令值得是一个执行某些特定事情的指令

2.应用场景

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

3.demo1-菜单程序

假设我们正在写一个用户界面程序,该用户至少有数十个button按钮,因为项目比较复杂,所以我们决定让某个程序员负责绘制这些按钮,而另外一些程序员则负责编写点击按钮后的具体行为,这些行为都将被封装在对象里
在大型项目开发中,这是很正常的分工。对于绘制按钮的程序员来说,他完全不知道某个按钮将来要做什么,可能刷新菜单界面,可能增加一些子菜单,他只知道点击这个按钮会发生某些事情,那么当完成这个按钮的绘制之后,应该如何给他绑定onclick事件呢?
回想一下命令模式的应用场景,我们很快可以找到这里运用命令模式的理由,点击按钮之后,必须向某些负责具体行为的对象发送请求,这些对象就是请求的接收者。但目前并不知道接收者是什么对象,也不知道接收者究竟会做什么,此时我们需要借助命令对象的帮助,以便解开按钮和负责具体行为对象之间的耦合。
设计模式的主题总是把不变的事情和变化的事情分离开来,命令模式也不例外。按下按钮之后会发生的一些事情是不变的,而具体会发生什么事情是可变的。通过command对象的帮助,将来我们可以轻易改变这种关联,因此也可以在将来再次改变按钮的行为。

html 复制代码
   <!-- 1.首先完成按钮绘制 -->
   <button id="button1">刷新菜单</button>
   <button id="button2">增加子菜单</button>
   <button id="button3">删除子菜单</button>
   <script>
    var button1=document.getElementById('button1');
    var button2=document.getElementById('button2');
    var button3=document.getElementById('button3');
    // 2.往按钮上面安装命令 -- 即setCommand函数,点击按钮,执行某个command命令,
    // 执行命令的动作为调用command对象的execute()方法
    // 负责绘制按钮的程序员不关心这些事情,他只需要预留好安装命令的接口,command对象自然知道如何和正确的对象沟通
    var setCommand = function(button,command) {
        button.onclick = function(){
            command.execute();
        };
    };
    // 3.负责编写点击之后的具体行为的程序员总算交上了他们的具体成果,完成刷新,新增子菜单,删除子菜单的功能
    // 他们分布在MenuBar和SubMenu这两个对象中
    var MenuBar = {
        refresh:function(){
            console.log('刷新菜单目录');
        }
    };
    var SubMenu = {
        add:function(){
            console.log('增加子菜单');
        },
        del:function(){
            console.log('删除子菜单');
        }
    };
    // 4.在让button变得有用之前,我们先把这些行为封装在命令类中
    var RefreshMenuBarCommand = function(receiver){
        this.receiver = receiver;
    };
    RefreshMenuBarCommand.prototype.execute=function(){
        this.receiver.refresh();
    };
    var AddSubMenuCommand = function(receiver){
        this.receiver=receiver;
    }
    AddSubMenuCommand.prototype.execute = function(){
        this.receiver.add();
    }
    var DelSubMenuCommand = function(receiver){
        this.receiver=receiver;
    }
    DelSubMenuCommand.prototype.execute = function(){
        this.receiver.del();
    }
    // 5.最后就是把命令接受者传入到command对象中,并且把command对象安装到button上面
    var refreshMenuBarCommand=new RefreshMenuBarCommand(MenuBar);
    var addSubMenuCommand =new AddSubMenuCommand(SubMenu);
    var delSubMenuCommand = new DelSubMenuCommand(SubMenu);

    setCommand(button1,refreshMenuBarCommand);
    setCommand(button2,addSubMenuCommand);
    setCommand(button3,delSubMenuCommand);
   </script>

4.命令模式的作用不仅仅是封装运算快,还可以很方便地给命令对象增加撤销操作哦,之前策略模式有写过一个Animal类,这个类是让页面上的div移动到水平方向的某个位置,下面来修改一下这个demo

现在页面中有一个input文本框和一个button按钮,文本框可以输入一些数字,表示小球移动后的水平位置,小球在用户点击按钮后立即开始移动,代码如下:

html 复制代码
	<div style="top:50px;position:absolute;background:#f20;width:50px;height:50px;border-radius:50%;" id="ball"></div>
    小球移动后的位置:<input type="text" id="pos">
    <button id="moveBtn">开始移动</button>
    <script>
       var tween={
        linear:function(t,b,c,d){
            return c*t/d +b;
        },
        easeIn:function(t,b,c,d){
            return c*(t/=d)*t+b;
        },
        strongEaseIn:function(t,b,c,d){
            return c*(t/=d)*t*t*t*t+b;
        },
        strongEaseOut:function(t,b,c,d){
            return c*((t=t/d-1)*t*t*t*t+1)+b;
        },
        sineaseIn:function(t,b,c,d){
            return c*(t/=d)*t*t+b;
        },
        sineaseOut:function(t,b,c,d){
            return c*((t=t/d-1)*t*t+1)+b;
        },
       };
       var Animate = function(dom){
        this.dom=dom; // 进行运动的dom节点
        this.startTime = 0; // 动画开始时间
        this.startPos = 0; // 动画开始时,dom节点的位置,即初始位置
        this.endPos = 0; // 动画结束时,dom节点的位置,即目标位置
        this.propertyName = null; // dom节点需要被改变的css属性名
        this.easing = null; // 缓动算法
        this.duration = null; // 动画持续时间 
       }
       Animate.prototype.start = function(propertyName,endPos,duration,easing){
        this.startTime=+new Date(); // 动画启动时间
        this.startPos = this.dom.getBoundingClientRect()[propertyName]; // dom节点初始位置
        this.propertyName = propertyName; // dom节点需要被改变的CSS属性名
        this.endPos = endPos; // dom节点目标位置
        this.duration=duration; // 动画持续时间
        this.easing = tween[easing]; // 缓动算法

        var self = this;
        var timeId = setInterval(function(){ // 启动定时器,开始执行动画
            if (self.step()===false){ // 如果动画已结束,清除定时器
                clearInterval(timeId);
            }
        },19)
       }

       Animate.prototype.step = function(){
        var t= +new Date; // 取得当前时间
        if(t>=this.startTime+this.duration){
            this.update(this.endPos); // 更新次小球的CSS属性值
            return false;
        }
        var pos = this.easing(t-this.startTime,this.startPos,this.endPos-this.startPos,this.duration);
        // pos为小球当前位置
        this.update(pos); // 更新小球的CSS属性值
       };

       Animate.prototype.update = function(pos){
        this.dom.style[this.propertyName]=pos+'px';
       };
       // 测试
       var ball = document.getElementById('ball');
       var pos=document.getElementById('pos');
       var moveBtn = document.getElementById('moveBtn');
       moveBtn.onclick=function(){
        var animate = new Animate(ball);
        animate.start('left',pos.value,1000,'sineaseOut');
       }
    </script>

现在我们想要加一个撤销按钮,以便小球可以会回到初始状态,我们先把目前代码改成用命令模式实现

html 复制代码
	<div style="top:50px;position:absolute;background:#f20;width:50px;height:50px;border-radius:50%;" id="ball"></div>
    小球移动后的位置:<input type="text" id="pos">
    <button id="moveBtn">开始移动</button>
    <script>
       var tween={
        linear:function(t,b,c,d){
            return c*t/d +b;
        },
        easeIn:function(t,b,c,d){
            return c*(t/=d)*t+b;
        },
        strongEaseIn:function(t,b,c,d){
            return c*(t/=d)*t*t*t*t+b;
        },
        strongEaseOut:function(t,b,c,d){
            return c*((t=t/d-1)*t*t*t*t+1)+b;
        },
        sineaseIn:function(t,b,c,d){
            return c*(t/=d)*t*t+b;
        },
        sineaseOut:function(t,b,c,d){
            return c*((t=t/d-1)*t*t+1)+b;
        },
       };
       var Animate = function(dom){
        this.dom=dom; // 进行运动的dom节点
        this.startTime = 0; // 动画开始时间
        this.startPos = 0; // 动画开始时,dom节点的位置,即初始位置
        this.endPos = 0; // 动画结束时,dom节点的位置,即目标位置
        this.propertyName = null; // dom节点需要被改变的css属性名
        this.easing = null; // 缓动算法
        this.duration = null; // 动画持续时间 
       }
       Animate.prototype.start = function(propertyName,endPos,duration,easing){
        this.startTime=+new Date(); // 动画启动时间
        this.startPos = this.dom.getBoundingClientRect()[propertyName]; // dom节点初始位置
        this.propertyName = propertyName; // dom节点需要被改变的CSS属性名
        this.endPos = endPos; // dom节点目标位置
        this.duration=duration; // 动画持续时间
        this.easing = tween[easing]; // 缓动算法

        var self = this;
        var timeId = setInterval(function(){ // 启动定时器,开始执行动画
            if (self.step()===false){ // 如果动画已结束,清除定时器
                clearInterval(timeId);
            }
        },19)
       }

       Animate.prototype.step = function(){
        var t= +new Date; // 取得当前时间
        if(t>=this.startTime+this.duration){
            this.update(this.endPos); // 更新次小球的CSS属性值
            return false;
        }
        var pos = this.easing(t-this.startTime,this.startPos,this.endPos-this.startPos,this.duration);
        // pos为小球当前位置
        this.update(pos); // 更新小球的CSS属性值
       };

       Animate.prototype.update = function(pos){
        this.dom.style[this.propertyName]=pos+'px';
       };
       // 测试
       var ball = document.getElementById('ball');
       var pos=document.getElementById('pos');
       var moveBtn = document.getElementById('moveBtn');

       var MoveCommand = function(receiver,pos){
        this.receiver = receiver;
        this.pos=pos;
       }
       MoveCommand.prototype.execute = function(){
        this.receiver.start('left',this.pos,1000,'sineaseOut');
       }
       var moveCommand;
       
       moveBtn.onclick=function(){
        var animate = new Animate(ball);
        moveCommand=new MoveCommand(animate,pos.value);
        moveCommand.execute();
       }
    </script>

增加撤销按钮

html 复制代码
	<div style="top:50px;position:absolute;background:#f20;width:50px;height:50px;border-radius:50%;" id="ball"></div>
    小球移动后的位置:<input type="text" id="pos">
    <button id="moveBtn">开始移动</button>
    <!-- 取消按钮 -->
    <button id="cancelBtn">取消</button>
    <script>
       var tween={
        linear:function(t,b,c,d){
            return c*t/d +b;
        },
        easeIn:function(t,b,c,d){
            return c*(t/=d)*t+b;
        },
        strongEaseIn:function(t,b,c,d){
            return c*(t/=d)*t*t*t*t+b;
        },
        strongEaseOut:function(t,b,c,d){
            return c*((t=t/d-1)*t*t*t*t+1)+b;
        },
        sineaseIn:function(t,b,c,d){
            return c*(t/=d)*t*t+b;
        },
        sineaseOut:function(t,b,c,d){
            return c*((t=t/d-1)*t*t+1)+b;
        },
       };
       var Animate = function(dom){
        this.dom=dom; // 进行运动的dom节点
        this.startTime = 0; // 动画开始时间
        this.startPos = 0; // 动画开始时,dom节点的位置,即初始位置
        this.endPos = 0; // 动画结束时,dom节点的位置,即目标位置
        this.propertyName = null; // dom节点需要被改变的css属性名
        this.easing = null; // 缓动算法
        this.duration = null; // 动画持续时间 
       }
       Animate.prototype.start = function(propertyName,endPos,duration,easing){
        this.startTime=+new Date(); // 动画启动时间
        this.startPos = this.dom.getBoundingClientRect()[propertyName]; // dom节点初始位置
        this.propertyName = propertyName; // dom节点需要被改变的CSS属性名
        this.endPos = endPos; // dom节点目标位置
        this.duration=duration; // 动画持续时间
        this.easing = tween[easing]; // 缓动算法

        var self = this;
        var timeId = setInterval(function(){ // 启动定时器,开始执行动画
            if (self.step()===false){ // 如果动画已结束,清除定时器
                clearInterval(timeId);
            }
        },19)
       }

       Animate.prototype.step = function(){
        var t= +new Date; // 取得当前时间
        if(t>=this.startTime+this.duration){
            this.update(this.endPos); // 更新次小球的CSS属性值
            return false;
        }
        var pos = this.easing(t-this.startTime,this.startPos,this.endPos-this.startPos,this.duration);
        // pos为小球当前位置
        this.update(pos); // 更新小球的CSS属性值
       };

       Animate.prototype.update = function(pos){
        this.dom.style[this.propertyName]=pos+'px';
       };
       // 测试
       var ball = document.getElementById('ball');
       var pos=document.getElementById('pos');
       var moveBtn = document.getElementById('moveBtn');
       var cancelBtn = document.getElementById('cancelBtn');

       var MoveCommand = function(receiver,pos){
        this.receiver = receiver;
        this.pos=pos;
        this.oldPos = null;
       }
       MoveCommand.prototype.execute = function(){
        this.receiver.start('left',this.pos,1000,'sineaseOut');
        // 记录小球开始移动前的位置
        this.oldPos = this.receiver.dom.getBoundingClientRect()[this.receiver.propertyName];
       }

       MoveCommand.prototype.undo=function(){
        // 回到小球移动前记录的位置
        this.receiver.start('left',this.oldPos,1000,'sineaseOut');
       }
       var moveCommand;
       
       moveBtn.onclick=function(){
        var animate = new Animate(ball);
        moveCommand=new MoveCommand(animate,pos.value);
        moveCommand.execute();
       }
       cancelBtn.onclick=function(){
        moveCommand.undo();
       }
    </script>

现在通过命令模式轻松实现了撤销功能,撤销是命令模式里一个非常有用的功能,试想开发一个围棋程序的时候,我们把每一步棋子的变化都封装成命令,则可以轻而易举的实现悔棋功能,同样撤销命令还可以用于文本编辑器的ctrl+z功能

5.命令模式实现播放录像功能

html 复制代码
<button id="replay">播放录像</button>
   <script>
    var Ryu = {
        attack:function(){
            console.log('攻击');
        },
        defense:function(){
            console.log('防御');
        },
        jump:function(){
            console.log('跳跃');
        },
        crouch:function(){
            console.log('蹲下');
        },
    }

    var makeCommand = function(receiver,state){
        return function(){
            receiver[state]();
        }
    }

    var commands={
        '119':'jump', // W
        '115':'crouch', // S
        '97':'defense', // A
        '100':'attack', // D
    };

    var commandStack=[];
    document.onkeypress = function(ev){
        var keyCode = ev.keyCode,
        command = makeCommand(Ryu,commands[keyCode]);
        if(command){
            command();
            commandStack.push(command);
        }
    };

    document.getElementById('replay').onclick=function(){
        var command;
        while(command=commandStack.shift()){
            command();
        }
    }
   </script>

6.宏命令,宏命令是一组命令的集合,通过执行宏命令的方式,可以一次执行一批命令,想象一下,家里有一个万能遥控器,每天回家的时候,只要按一个特别的按钮,他就会帮我们关上房间门,顺便打开电脑并登陆QQ.

javascript 复制代码
	var closeDoorCommand = {
        execute:function(){
            console.log('关门')
        }
    };
    var openDoorCommand = {
        execute:function(){
            console.log('开门')
        }
    }
    var openQQCommand = {
        execute:function(){
            console.log('登陆QQ')
        }
    }
    var MacroCommand = function(){
        return {
            commandsList:[],
            add:function(command){
                this.commandsList.push(command);
            },
            execute:function(){
                for(let i=0,command;command=this.commandsList[i++];){
                    command.execute();
                }
            }
        }
    }

    var macroCommand = MacroCommand();
    macroCommand.add(closeDoorCommand);
    macroCommand.add(openDoorCommand);
    macroCommand.add(openQQCommand);
    macroCommand.execute();

宏命令是命令模式和组合模式的产物

非原创,内容来源javascript设计模式与开发实践 -曾探

相关推荐
有一个好名字2 小时前
设计模式-工厂方法模式
java·设计模式·工厂方法模式
阿波罗尼亚4 小时前
Head First设计模式(十三) 设计原则 现实世界中的模式
设计模式
sg_knight4 小时前
Python 中的常用设计模式工具与库
开发语言·python·设计模式
雨中飘荡的记忆1 天前
享元模式深度解析:看Java如何优雅节省内存
java·设计模式
How_doyou_do1 天前
Agent设计模式与工程化
设计模式
_膨胀的大雄_1 天前
01-创建型模式
前端·设计模式
是2的10次方啊1 天前
🎭 程序员的周末:11种设计模式继续藏在你身边
设计模式
kylezhao20191 天前
C#23种设计模式-单例模式(Singleton)详解与应用
单例模式·设计模式·c#
会员果汁1 天前
5.设计模式-工厂方法模式
设计模式·工厂方法模式