命令模式

[TOC]

原理

示例

菜单程序(Js实现)

需求:

假设我们正在编写一个用户界面程序,该用户界面上至少有数十个Button 按钮。因为项目比较复杂,所以我们决定让某个程序员负责绘制这些按钮,而另外一些程序员则负责编写点击按钮后的具体行为,这些行为都将被封装在对象里。在大型项目开发中,这是很正常的分工。对于绘制按钮的程序员来说,他完全不知道某个按钮未来将用来做什么,可能用来刷新菜单界面,也可能用来增加一些子菜单,他只知道点击这个按钮会发生某些事情

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

设计模式的主题总是把不变的事物和变化的事物分离开来,命令模式也不例外。按下按钮之后会发生一些事情是不变的,而具体会发生什么事情是可变的。通过command 对象的帮助,将来我们可以轻易地改变这种关联,因此也可以在将来再次改变按钮的行为

传统实现

下面进入代码编写阶段

首先在页面中完成这些按钮的“绘制”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  <html>

<head>
<title></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');
</script>
</body>

</html>

接下来定义setCommand 函数,setCommand 函数负责往按钮上面安装命令。可以肯定的是,点击按钮会执行某个command 命令,执行命令的动作被约定为调用command 对象的execute()方法。
虽然还不知道这些命令究竟代表什么操作,但负责绘制按钮的程序员不关心这些事情,他只需要预留好安装命令的接口,command 对象自然知道如何和正确的对象沟通:

1
2
3
4
5
var setCommand = function (button, command) {
button.onclick = function () {
command.execute();
}
};

最后,负责编写点击按钮之后的具体行为的程序员总算交上了他们的成果,他们完成了刷新菜单界面、增加子菜单和删除子菜单这几个功能,这几个功能被分布在MenuBar 和SubMenu 这两个对象中:

1
2
3
4
5
6
7
8
9
10
11
12
13
var MenuBar = {
refresh: function () {
console.log('刷新菜单目录');
}
};
var SubMenu = {
add: function () {
console.log('增加子菜单');
},
del: function () {
console.log('删除子菜单');
}
};

在让button 变得有用起来之前,我们要先把这些行为都封装在命令类中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 上面:

1
2
3
4
5
6
var refreshMenuBarCommand = new RefreshMenuBarCommand(MenuBar);
var addSubMenuCommand = new AddSubMenuCommand(SubMenu);
var delSubMenuCommand = new DelSubMenuCommand(SubMenu);
setCommand(button1, refreshMenuBarCommand);
setCommand(button2, addSubMenuCommand);
setCommand(button3, delSubMenuCommand);

JavaScript 中的命令模式

也许我们会感到很奇怪,所谓的命令模式,看起来就是给对象的某个方法取了execute 的名字。引入command 对象和receiver 这两个无中生有的角色无非是把简单的事情复杂化了,即使不用什么模式,用下面寥寥几行代码就可以实现相同的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var bindClick = function (button, func) {
button.onclick = func;
};
var MenuBar = {
refresh: function () {
console.log('刷新菜单界面');
}
};
var SubMenu = {
add: function () {
console.log('增加子菜单');
},
del: function () {
console.log('删除子菜单');
}
};

bindClick(button1, MenuBar.refresh);
bindClick(button2, SubMenu.add);
bindClick(button3, SubMenu.del);

其实,命令模式的由来,其实是回调(callback)函数的一个面向对象的替代品。
JavaScript 作为将函数作为一等对象的语言,跟策略模式一样,命令模式也早已融入到了JavaScript 语言之中。运算块不一定要封装在command.execute 方法中,也可以封装在普通函数中。函数作为一等对象,本身就可以被四处传递。即使我们依然需要请求“接收者”,那也未必使用面向对象的方式,闭包可以完成同样的功能。

在面向对象设计中,命令模式的接收者被当成command 对象的属性保存起来,同时约定执行命令的操作调用command.execute 方法。在使用闭包的命令模式实现中,接收者被封闭在闭包产生的环境中,执行命令的操作可以更加简单,仅仅执行回调函数即可。无论接收者被保存为对象的属性,还是被封闭在闭包产生的环境中,在将来执行命令的时候,接收者都能被顺利访问。用
闭包实现的命令模式如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var setCommand = function( button, func ){
button.onclick = function(){
func();
}
};
var MenuBar = {
refresh: function(){
console.log( '刷新菜单界面' );
}
};
var RefreshMenuBarCommand = function( receiver ){
return function(){
receiver.refresh();
}
};
var refreshMenuBarCommand = RefreshMenuBarCommand( MenuBar );
setCommand( button1, refreshMenuBarCommand );

如果想更明确地表达当前正在使用命令模式,或者除了执行命令之外,将来有可能还要提供撤销命令等操作。那我们最好还是把执行函数改为调用execute 方法:

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

计算器(C#)实现

设计一个计算器具有以下功能

  • 可以进行整数的加减乘除
  • 用户可以撤销操作
  • 用户可以重新执行撤销的操作

代码如下:

Command类

1
2
3
4
5
6
7
8
/// <summary>
/// "Command"
/// </summary>
public abstract class Command
{
public abstract void Execute();
public abstract void UnExecute();
}

Calculator类(Receiver)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/// <summary>
/// "Receiver"
/// </summary>
public class Calculator
{
private int _curr = 0;
public void Operation(char @operator, int operand)
{
switch (@operator)
{
case '+': _curr += operand; break;
case '-': _curr -= operand; break;
case '*': _curr *= operand; break;
case '/': _curr /= operand; break;
}
Console.WriteLine($"Current value = {_curr} (afterOperating: {@operator} {operand})");
}
}

CalculatorCommand类(ConcreteCommand)

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
45
46
47
48
49
/// <summary>
/// "ConcreteCommand"
/// </summary>
public class CalculatorCommand : Command
{
private readonly Calculator _calculator;
private readonly char _operator;
private readonly int _operand;

public CalculatorCommand(Calculator calculator, char @operator, int operand)
{
_operator = @operator;
_operand = operand;
_calculator = calculator;
}

public override void Execute()
{
_calculator.Operation(_operator, _operand);
}
public override void UnExecute()
{
_calculator.Operation(Undo(_operator), _operand);
}

private char Undo(char @operator)
{
char undo;
switch (@operator)
{
case '+':
undo = '-';
break;
case '-':
undo = '+';
break;
case '*':
undo = '/';
break;
case '/':
undo = '*';
break;
default:
undo = ' ';
break;
}
return undo;
}
}

User类(Invoker)

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
45
46
47
48
49
/// <summary>
/// "Invoker"
/// </summary>
public class User
{
private readonly Calculator _calculator = new Calculator();
private readonly List<Command> _commandList = new List<Command>();
private int _current = 0;

public void Redo(int levels)
{
Console.WriteLine("\n---- Redo {0} levels ", levels);

for (int i = 0; i < levels; i++)
{
if (_current >= _commandList.Count - 1)
{
break;
}
var command = _commandList[_current++];
command.Execute();
}
}

public void Undo(int levels)
{
Console.WriteLine("\n---- Undo {0} levels ", levels);
// Perform undo operations
for (int i = 0; i < levels; i++)
{
if (_current <= 0)
{
break;
}
var command = _commandList[--_current];
command.UnExecute();
}
}

public void Compute(char @operator, int operand)
{
// Create command operation and execute it
Command command = new CalculatorCommand(_calculator, @operator, operand);
command.Execute();
// Add command to undo list
_commandList.Add(command);
_current++;
}
}

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Program
{
static void Main(string[] args)
{
// Create user and let her compute
User user = new User();
user.Compute('+', 100);
user.Compute('-', 50);
user.Compute('*', 10);
user.Compute('/', 2);
// Undo 4 commands
user.Undo(4);
// Redo 3 commands
user.Redo(3);
// Wait for user
Console.Read();
}
}

盒马生鲜(python实现)

David:听说阿里开了一家实体店——盒马鲜生,特别火爆!明天就周末了,我们一起去吃大闸蟹吧! Tony:吃货!真是味觉的哥伦布啊,哪里的餐饮新店都少不了你的影子。不过听说盒马鲜生到处是黑科技诶,而且海生是自己挑的,还满新奇的。

David:那就说好了,明天 11:00,盒马鲜生,不吃不散!

Tony 和 David 来到杭州上城区的一家分店。这里食客众多,物品丰富,特别是生鲜,从几十块钱的小龙虾到几百块的大青蟹,再到一千多的俄罗斯帝王蟹,应有尽有。帝王蟹是吃不起了,Tony 和 David 挑了一只 900g 的一号大青蟹。

食材挑好了,接下来就是现厂加工。加工的方式有多种,清蒸、姜葱炒、香辣炒、避风塘炒等,可以任意选择,当然不同的方式价格也有所不同。因为我们选的蟹是当时活动推荐的,所以免加工费。选择一种加工方式后进行下单,下单后会给你一个呼叫器,厨师做好了会有专门的服务人员送过来,坐着等就可以了……


盒马鲜生之所以这么火爆,一方面是因为中国从来就不缺像 David 这样的吃货,另一方面是因为里面的海生很新鲜,而且可以自己挑选。很多人都喜欢吃大闸蟹,但是你有没有注意到一个问题?从你买大闸蟹到吃上大闸蟹的整个过程,可能都没有见过厨师,而你却能享受美味的佳肴。这里有一个很重要的角色就是服务员,她帮你下订单,然后把订单传送给厨师,厨师收到订单后根据订单做餐。我们用代码来模拟一下这个过程。

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
from abc import ABCMeta, abstractmethod

# 引入ABCMeta和abstractmethod来定义抽象类和抽象方法


class Chef():
"厨师"

def steamFood(self, originalMaterial):
print(originalMaterial + "清蒸中...")
return "清蒸" + originalMaterial

def stirFriedFood(self, originalMaterial):
print(originalMaterial + "爆炒中...")
return "香辣炒" + originalMaterial


class Order(metaclass=ABCMeta):
"订单"

def __init__(self, name, originalMaterial):
self._chef = Chef()
self._name = name
self._originalMaterial = originalMaterial

def getDisplayName(self):
return self._name + self._originalMaterial

@abstractmethod
def processingOrder(self):
pass


class SteamedOrder(Order):
"清蒸"

def __init__(self, originalMaterial):
super().__init__("清蒸", originalMaterial)

def processingOrder(self):
if (self._chef is not None):
return self._chef.steamFood(self._originalMaterial)
return ""


class SpicyOrder(Order):
"香辣炒"

def __init__(self, originalMaterial):
super().__init__("香辣炒", originalMaterial)

def processingOrder(self):
if (self._chef is not None):
return self._chef.stirFriedFood(self._originalMaterial)
return ""


class Waiter:
"服务员"

def __init__(self, name):
self.__name = name
self.__order = None

def receiveOrder(self, order):
self.__order = order
print("服务员" + self.__name + ":您的 " + order.getDisplayName() +
" 订单已经收到,请耐心等待")

def placeOrder(self):
food = self.__order.processingOrder()
print("服务员" + self.__name + ":您的餐 " + food + " 已经准备好,请您慢用!")

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def testOrder():
waiter = Waiter("Anna")
steamedOrder = SteamedOrder("大闸蟹")
print("客户David:我要一份" + steamedOrder.getDisplayName())
waiter.receiveOrder(steamedOrder)
waiter.placeOrder()
print()

spicyOrder = SpicyOrder("大闸蟹")
print("客户Tony:我要一份" + steamedOrder.getDisplayName())
waiter.receiveOrder(spicyOrder)
waiter.placeOrder()


testOrder()

测试结果

1
2
3
4
5
6
7
8
9
客户David:我要一份清蒸大闸蟹
服务员Anna:您的 清蒸大闸蟹 订单已经收到,请耐心等待
大闸蟹清蒸中...
服务员Anna:您的餐 清蒸大闸蟹 已经准备好,请您慢用!

客户Tony:我要一份清蒸大闸蟹
服务员Anna:您的 香辣炒大闸蟹 订单已经收到,请耐心等待
大闸蟹爆炒中...
服务员Anna:您的餐 香辣炒大闸蟹 已经准备好,请您慢用!

游戏(python实现)

在游戏中,有两个最基本的动作,一个是行走(也叫移动),一个是攻击。这几乎是所有游戏都少不了的基础功能,不然就没法玩了!

现在我们来模拟一下游戏角色(英雄)中的移动和攻击,为简单起见,假设移动只有上移(U)、下移(D)、左移(L)、右移(R)、上跳(J)、下蹲(S)这 6 个动作,而攻击(A)只有 1 种,括号中字符代表每一个动作在键盘中的按键,也就是对应动作的调用,这些动作的命令可以单独使用,但更多的时候会组合在一起使用。比如,弹跳就是上跳 + 下蹲两个的动作的组合,我们用 JP 表示;而弹跳攻击是弹跳 + 攻击的组合(也就是上跳 + 攻击 + 下蹲),我们用 JA 表示;而移动也可以两个方向一起移动,如上移 + 右移,我们用 RU 表示。下面的程序中,为简单起见,这里用标准输入的字符来代表按键输入事件。

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
import time
from abc import ABCMeta, abstractmethod


class GameRole:

# 每次移动的步距
STEP = 5

def __init__(self):
self.__x = 0
self.__y = 0
self.__z = 0

def leftMove(self):
self.__x -= self.STEP

def rightMove(self):
self.__x += self.STEP

def upMove(self):
self.__y += self.STEP

def downMove(self):
self.__y -= self.STEP

def jumpMove(self):
self.__z += self.STEP

def squatMove(self):
self.__z -= self.STEP

def attack(self):
print("攻击...")

def showPosition(self):
print("x:" + str(self.__x) + ", y:" +
str(self.__y) + ", z:" + str(self.__z))


class GameCommand(metaclass=ABCMeta):
"游戏角色的命令类"

def __init__(self, role):
self._role = role

def setRole(self, role):
self._role = role

@abstractmethod
def execute(self):
pass


class Left(GameCommand):
"左移命令"

def execute(self):
self._role.leftMove()
self._role.showPosition()


class Right(GameCommand):
"右移命令"

def execute(self):
self._role.rightMove()
self._role.showPosition()


class Up(GameCommand):
"上移命令"

def execute(self):
self._role.upMove()
self._role.showPosition()


class Down(GameCommand):
"下移命令"

def execute(self):
self._role.downMove()
self._role.showPosition()


class Jump(GameCommand):
"弹跳命令"

def execute(self):
self._role.jumpMove()
self._role.showPosition()
# 跳起后空中停留半秒
time.sleep(0.5)


class Squat(GameCommand):
"下蹲命令"

def execute(self):
self._role.squatMove()
self._role.showPosition()
# 下蹲后伏地半秒
time.sleep(0.5)


class Attack(GameCommand):
"攻击命令"

def execute(self):
self._role.attack()


class MacroCommand(GameCommand):

def __init__(self, role=None):
super().__init__(role)
self.__commands = []

def addCommand(self, command):
# 让所有的命令作用于同一个对象
# command.setRole(self._role)
self.__commands.append(command)

def removeCommand(self, command):
self.__commands.remove(command)

def execute(self):
for command in self.__commands:
command.execute()


class GameInvoker:
def __init__(self):
self.__command = None

def setCommand(self, command):
self.__command = command
return self

def action(self):
if self.__command is not None:
self.__command.execute()


def testGame():
role = GameRole()
invoker = GameInvoker()
while True:
strCmd = input("请输入命令:")
strCmd = strCmd.upper()
if (strCmd == "L"):
invoker.setCommand(Left(role)).action()
elif (strCmd == "R"):
invoker.setCommand(Right(role)).action()
elif (strCmd == "U"):
invoker.setCommand(Up(role)).action()
elif (strCmd == "D"):
invoker.setCommand(Down(role)).action()
elif (strCmd == "JP"):
cmd = MacroCommand()
cmd.addCommand(Jump(role))
cmd.addCommand(Squat(role))
invoker.setCommand(cmd).action()
elif (strCmd == "A"):
invoker.setCommand(Attack(role)).action()
elif (strCmd == "LU"):
cmd = MacroCommand()
cmd.addCommand(Left(role))
cmd.addCommand(Up(role))
invoker.setCommand(cmd).action()
elif (strCmd == "LD"):
cmd = MacroCommand()
cmd.addCommand(Left(role))
cmd.addCommand(Down(role))
invoker.setCommand(cmd).action()
elif (strCmd == "RU"):
cmd = MacroCommand()
cmd.addCommand(Right(role))
cmd.addCommand(Up(role))
invoker.setCommand(cmd).action()
elif (strCmd == "RD"):
cmd = MacroCommand()
cmd.addCommand(Right(role))
cmd.addCommand(Down(role))
invoker.setCommand(cmd).action()
elif (strCmd == "LA"):
cmd = MacroCommand()
cmd.addCommand(Left(role))
cmd.addCommand(Attack(role))
invoker.setCommand(cmd).action()
elif (strCmd == "RA"):
cmd = MacroCommand()
cmd.addCommand(Right(role))
cmd.addCommand(Attack(role))
invoker.setCommand(cmd).action()
elif (strCmd == "UA"):
cmd = MacroCommand()
cmd.addCommand(Up(role))
cmd.addCommand(Attack(role))
invoker.setCommand(cmd).action()
elif (strCmd == "DA"):
cmd = MacroCommand()
cmd.addCommand(Down(role))
cmd.addCommand(Attack(role))
invoker.setCommand(cmd).action()
elif (strCmd == "JA"):
cmd = MacroCommand()
cmd.addCommand(Jump(role))
cmd.addCommand(Attack(role))
cmd.addCommand(Squat(role))
invoker.setCommand(cmd).action()
elif (strCmd == "Q"):
exit()


testGame()

在上面的 Demo 中 MacroCommand 是一种组合命令,也叫宏命令(Macro Command)。宏命令是一个具体命令类,它拥有一个集合属性,在该集合中包含了对其他命令对象的引用,如上面的弹跳命令是上跳、攻击、下蹲 3 个命令的组合,引用了 3 个命令对象。

当调用宏命令的 execute() 方法时,会循环地调用每一个子命令的 execute() 方法。一个宏命令的成员可以是简单命令,还可以继续是宏命令,宏命令将递归地调用它所包含的每个成员命令的 execute() 方法。