命令模式
命令模式
命令模式的用途
命令模式是最简单和优雅的模式之一,命令模式中的命令(command)指的是一个执行某些特定事情的指令。
命令模式最常见的应用场景是:有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么。此时希望用一种松耦合的方式来设计程序,使得请求发送者和请求接收者能够消除彼此之间的耦合关系。
拿订餐来说,客人需要向厨师发送请求,但是完全不知道这些厨师的名字和联系方式,也不知道厨师炒菜的方式和步骤。命令模式把客人订餐的请求封装成 command 对象,也就是订餐中的订单对象。这个对象可以在程序中被四处传递,就像订单可以从服务员手中传到厨师的手中。这样一来,客人不需要知道厨师的名字,从而解开了请求调用者和请求接收者之间的耦合关系。
另外,相对于过程化的请求调用,command 对象拥有更长的生命周期。对象的生命周期是跟初始请求无关的,因为这个请求已经被封装在了 command 对象的方法中,成为了这个对象的行为。我们可以在程序运行的任意时刻去调用这个方法,就像厨师可以在客人预定 1 个小时之后才帮他炒菜,相当于程序在 1 个小时之后才开始执行 command 对象的方法。
除了这两点之外,命令模式还支持撤销、排队等操作。
命令模式的例子——菜单程序
- 传统方式实现
<html>
<body>
<button id="button1">点击按钮 1</button>
<button id="button2">点击按钮 2</button>
<button id="button3">点击按钮 3</button>
</body>
<script>
var button1 = document.getElementById('button1'),
button2 = document.getElementById('button2'),
button3 = document.getElementById('button3');
//定义 setCommand 函数,setCommand 函数负责往按钮上面安装命令
var setCommand = function (button, command) {
button.onclick = function () {
command.execute();
}
};
// 最后,负责编写点击按钮之后的具体行为的程序员总算交上了他们的成果,他们完成了刷新菜单界面、增加子菜单和删除子菜单这几个功能,这几个功能被分布在 MenuBar 和SubMenu 这两个对象中:
var MenuBar = {
refresh: function () {
console.log('刷新菜单目录');
}
};
var SubMenu = {
add: function () {
console.log('增加子菜单');
},
del: function () {
console.log('删除子菜单');
}
};
// 在让 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 () {
console.log('删除子菜单');
};
// 最后就是把命令接收者传入到 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>
</html>
- javascript 方式
在面向对象设计中,命令模式的接收者被当成 command 对象的属性保存起来,同时约定执行命令的操作调用 command.execute 方法。在使用闭包的命令模式实现中,接收者被封闭在闭包产生的环境中,执行命令的操作可以更加简单,仅仅执行回调函数即可。无论接收者被保存为对象的属性,还是被封闭在闭包产生的环境中,在将来执行命令的时候,接收者都能被顺利访问。用闭包实现的命令模式如下代码所示:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
</head>
<body>
<button id="button1">点击按钮 1</button>
<script>
var RefreshMenuBarCommand = function(receiver) {
return {
execute: function() {
receiver.refresh();
}
}
};
var setCommand = function(button, command) {
button.onclick = function() {
command.execute();
}
};
var MenuBar = {
refresh: function() {
console.log('刷新菜单界面');
}
};
var button1 = document.getElementById('button1')
var refreshMenuBarCommand = RefreshMenuBarCommand(MenuBar);
setCommand(button1, refreshMenuBarCommand);
</script>
</body>
</html>
撤销命令
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
</head>
<body>
<body>
<div id="ball" style="position:absolute;background:#000;width:50px;height:50px"></div>
输入小球移动后的位置:<input id="pos" />
<button id="moveBtn">开始移动</button>
<button id="cancelBtn">cancel</cancel>
<!--增加取消按钮-->
</body>
<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 节点的位置,即 dom 的初始位置
this.endPos = 0;
// 动画结束时,dom 节点的位置,即 dom 的目标位置
this.propertyName = null;
// dom 节点需要被改变的 css 属性名
this.easing = null;
// 缓动算法
this.duration = null;
// 动画持续时间
};
// Animate.prototype.start 方法负责启动这个动画,在动画被启动的瞬间,要记录一些信息,供缓动算法在以后计算小球当前位置的时候使用。在记录完这些信息之后,此方法还要负责启动定时器。
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 方法,该方法代表小球运动的每一帧要做的事情。在此处,这个方法负责计算小球的当前位置和调用更新 CSS 属性值的方法
Animate.prototype.step = function() {
var t = +new Date;
// 取得当前时间
if (t >= this.startTime + this.duration) {
// (1)如果当前时间大于动画开始时间加上动画持续时间之和,说明动画已经结束,此时要修正小球的位置。因为在这一帧开始之后,小球的位置已经接近了目标位置,但很可能不完全等于目标位置。
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, 'strongEaseOut');
this.oldPos = this.receiver.dom.getBoundingClientRect()[this.receiver.propertyName];
// 记录小球开始移动前的位置
};
MoveCommand.prototype.undo = function() {
this.receiver.start('left', this.oldPos, 1000, 'strongEaseOut');
// 回到小球移动前记录的位置
};
var moveCommand;
moveBtn.onclick = function() {
var animate = new Animate(ball);
moveCommand = new MoveCommand(animate, pos.value);
moveCommand.execute();
};
cancelBtn.onclick = function() {
// 撤销命令
moveCommand.undo();
};
</script>
</body>
</html>
撤销和重做
把用户在键盘的输入都封装成命令,执行过的命令将被存放到堆栈中。播放录像的时候只需要从头开始依次执行这些命令便可
<html>
<body>
<button id="replay">播放录像</button>
</body>
<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>
</html>
宏命令
宏命令是一组命令的集合,通过执行宏命令的方式,可以一次执行一批命令
var closeDoorCommand = {
execute: function () {
console.log('关门');
}
};
var openPcCommand = {
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 (var i = 0, command; command = this.commandsList[i++];) {
command.execute();
}
}
}
};
var macroCommand = MacroCommand();
macroCommand.add(closeDoorCommand);
macroCommand.add(openPcCommand);
macroCommand.add(openQQCommand);
macroCommand.execute();
// 关门
// 开电脑
// 登录 QQ