命令模式

将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作

应用场景

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

菜单案例

场景

假设我们正在编写一个用户界面程序,该用户界面上至少有数十个 Button 按钮。因为项目比较复杂,所以我们决定让某个程序员负责绘制这些按钮,而另外一些程序员则负责编写点击按钮后的具体行为,这些行为都将被封装在对象里。

在大型项目开发中,这是很正常的分工。对于绘制按钮的程序员来说,他完全不知道某个按钮未来将用来做什么,可能用来刷新菜单界面,也可能用来增加一些子菜单,他只知道点击这个 按钮会发生某些事情。那么当完成这个按钮的绘制之后,应该如何给它绑定onclick 事件呢?

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

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let RefreshMenuBarCommand = function (receiver) {
return {
execute: function () {
receiver.refresh();
}
}
};
let setCommand = function (button, command) {
button.onclick = function () {
command.execute();
}
};
let refreshMenuBarCommand = RefreshMenuBarCommand(MenuBar);
setCommand(button1, refreshMenuBarCommand);

撤销和重做命令

场景

命令模式的作用不仅是封装运算块,而且可以很方便地给命令对象增加撤销操作。现在页面上有游戏需要对已经进行的操作进行回放

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
let Ryu = {
attack() {
console.log("攻击");
},
defense() {
console.log("防御");
},
jump() {
console.log("跳跃");
},
crouch() {
console.log("蹲下");
},
}

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

let commands = {
"119": "jump", // w
"115": "crouch", // s
"97": "defense", // a
"100": "attack", // d
}

let commandStack = []; // 保存命令的堆栈

document.onkeypress = function (e) {
let keyCode = e.keyCode;
let command = makeCommand(Ryu, commands[keyCode]);

if (command) {
command();
commandStack.push(command);
}
}

document.getElementById("replay").onclick = function () { // 点击播放录像
let command;
while (command = commandStack.shift()) { // 从堆栈获取此前的命令并执行
command();
}
}

《javascript设计模式与开发实践》书上的这个例子写的更像是策略模式,书上为了和策略模式区分还把receiver[state]()用一个匿名函数给包起来,被我给删了,不要为了用设计模式而用设计模式。其实策略模式是对一系列算法的封装,使他们互相可替换,但命令模式是对一系列命令对象的封装,使接受者和发送者解耦

宏命令

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
let closeDoorCommand = {
execute: function () {
console.log('关门');
}
};
let openPcCommand = {
execute: function () {
console.log('开电脑');
}
};
let openQQCommand = {
execute: function () {
console.log('登录 QQ');
}
};
let MacroCommand = function () {
return {
commandsList: [],
add: function (command) { // 给宏命令添加指令
this.commandsList.push(command);
},
execute: function () { // 执行宏命令
for (let i = 0, command; command = this.commandsList[i++];) {
command.execute();
}
}
}
};
let macroCommand = MacroCommand();

macroCommand.add(closeDoorCommand);
macroCommand.add(openPcCommand);
macroCommand.add(openQQCommand);

macroCommand.execute();

智能命令与傻瓜命令

​ 上一个例子的命令没用包含任何receiver的信息,它本身就包揽了执行请求的行为,这跟我们之前看到的命令对象都包含了一个receiver是矛盾的。

​ 一般来说,命令模式都会在command对象中保存一个接受者来负责真正执行客户的请求,这种情况下命令对象是“傻瓜式”的,它只负责把客户的请求转交给接受者来执行,这种模式的好处是请求的发起者和请求的接受者之间尽可能的解耦。

​ “聪明”的命令对象可以直接实现请求,这样一来就不需要接收者的存在,和策略模式非常接近,但命令模式解决的问题更具发散性。

​ 命令模式还可以完成撤销、排队等功能

参考

《javascript设计模式与开发实践》

作者

Liang

发布于

2021-02-03

更新于

2021-12-25

许可协议


评论