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设计模式与开发实践 -曾探