命令模式主要用于解耦请求者与执行者之间的关系。有些场景,请求者并不需要知道操作的具体执行者、操作具体怎么做,但是仍需要发送请求。命令模式就很适合这种场景(比如,订餐)。
传统面向对象设计语言实现命令模式的两个关键点是命令对象command
以及命令类***Command
。具体的执行者被封装成命令类的receiver
属性,在execute
方法内执行receiver.xxx
具体的操作(将执行操作被封装成command
的execute
方法)。请求者会持有command
,发起请求就调用command.execute
方法。命名是相互约定的,不是固定的。
这样就形成了一种松耦合的关系,请求者与执行者之间通过command
维持联系,其他信息相互隔离开来,可以将二者(请求者、执行者)分工同时进行。但是这种实现方式会创建很多命令类,看下例。
JS 模拟传统面向对象语言实现命令模式:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</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');
// 菜单例子 - 模拟传统面向对象语言 - 命令模式
// 请求者只需要执行 setCommand 函数, 持有 command 对象
function setCommand(button, command) {
button.onclick = function() {
command.execute();
}
}
// 具体的执行者,以及具体的操作
var MenuBar = {
refresh() {
console.log('刷新菜单目录');
}
}
var SubMenu = {
add() {
console.log('增加子菜单');
},
del() {
console.log('删除子菜单');
}
}
// 为每个执行者的每个具体操作都创建对应的命令类,并创建约定好的 execute 方法
// 本例中有 2 个执行者,3 个具体操作,相当于要创建 3 个命令类
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();
}
// 通过 setCommand 给每个按钮添加 command 对象
setCommand(button1, new RefreshMenuBarCommand(MenuBar));
setCommand(button2, new AddSubMenuCommand(SubMenu));
setCommand(button3, new DelSubMenuCommand(SubMenu));
</script>
</body>
</html>
JS
不同,不需要这么复杂。通过 高阶函数 + 闭包 就可以实现。闭包函数可以像command
对象一样,通过调用闭包函数就可以替代调用command.execute
这一操作。优缺点都是一样的(松耦合、很多命令类)。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</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');
// 菜单例子 - 命令模式 - JS 自身特点
// 用 函数 代替了 command 对象
function setCommand (button, execute) {
button.onclick = function() {
execute();
}
}
// 具体的执行者,以及具体的操作
var MenuBar = {
refresh() {
console.log('刷新菜单目录');
}
}
var SubMenu = {
add() {
console.log('增加子菜单');
},
del() {
console.log('删除子菜单');
}
}
// 命令类的实现用 高阶函数 + 闭包 完成
var RefreshMenuBarCommand = function (receiver) {
return function() {
receiver.refresh();
}
}
var AddSubMenuCommand = function(receiver) {
return function() {
receiver.add();
}
}
var DelSubMenuCommand = function(receiver) {
return function() {
receiver.del();
}
}
var refreshMenuBarCommand = RefreshMenuBarCommand(MenuBar);
var addSubMenuCommand = AddSubMenuCommand(SubMenu);
var delSubMenuCommand = DelSubMenuCommand(SubMenu);
setCommand(button1, refreshMenuBarCommand);
setCommand(button2, addSubMenuCommand);
setCommand(button3, delSubMenuCommand);
</script>
</body>
</html>
还可以实现撤销和重做功能。
撤销操作一般约定命名为unexecute or undo
,主要实现思路是在执行execute
之前保存下请求者的状态,在执行unexecute
方法时,利用保存的状态还原。
撤销一般只能撤销一步。撤销一系列命令或者操作的思路是重做
。主要实现思路是将请求者发起的命令保存在一个历史记录队列中,在redo
的时候先重置当前状态,然后执行历史记录队列中的命令。
动画场景用的较多的队列功能。
如果用户的点击过快,可能会导致第一个动画突然停止,立马切到第二个动画。这种体验肯定不好,最好的体验肯定是流畅且连贯的画面。命令对象的生命周期是较早且长的,可以将HTML
元素的动画过程封装成命令对象,然后压进队列,当一个任务执行完,再通知(通知机制可以用回调函数
或发布-订阅
)队列,弹出并执行下一个任务。
宏命令就是一些列命令的组合,调用一个宏命令就就可以执行一批命令。与之前的实现不同,命令类没有receiver
属性,execute
方法没有定义在原型上,而是定义为自身属性。这种实现方式叫智能命令,而之前的实现都是傻瓜式命令。
// 宏命令 - JS 自身特点实现
// 场景:万能遥控器的一个特别按钮, 可以干 3 件事,关门、打开电脑、登录QQ
// 首先创建好各种 command 对象
var closeDoorCommand = {
execute() {
console.log(('关门'));
}
}
var openPcCommand = {
execute() {
console.log('开电脑');
}
}
var openQQCommand = {
execute() {
console.log('登录QQ');
}
}
// 然后定义 宏命令
var MacroCommand = function() {
return {
commandList: [],
add(command) {
this.commandList.push(command);
},
execute() {
this.commandList.forEach(command => command.execute());
}
}
}
// 创建宏命令对象并添加子命令
var macroCommand = MacroCommand();
macroCommand.add(closeDoorCommand);
macroCommand.add(openPcCommand);
macroCommand.add(openQQCommand);
macroCommand.execute();