将一个请求(request)封装成一个对象,从而允许你使用不同的请求、队列或日志将客户端参数化,同时支持请求操作的撤销与恢复。
命令就是一个对象化(实例化)的方法调用(A command is a reified method call)。
命令就是面向对象化的回调(Commands are an object-oriented replacement for callbacks)。
(摘自《游戏编程模式》)
简单的例子
上面对于命令模式的定义该怎么理解呢?我们用一个很简单的例子进行介绍。一个简单的if-else实现的“满足条件->执行事件”功能,我们可以这么写:
if(A is True)
DueA()
else if(B is True)
DueB()
上面的代码是一种硬编码的方式。其问题在于,当我们想要改变某个条件下执行的事件内容,需要重新修改代码并编译出可执行文件。若是以这种方式编写游戏的按键输入事件,用户就不可以自定义按键映射,然而现实是,许多游戏允许用户配置他们的按键与游戏行为之间的映射关系。因此,将DueA和DueB这类方法事件包装成一个统一父类的命令,便可以实现自定义配置的功能。修改后的代码如下:
class Command
{
public:
Excute();
}
class Command1 : Command
{
public:
Excute();
}
class Command2 : Command
{
public:
Excute();
}
Command* commondA;
Command* commondB;
//if...else可写为
if(A is True)
commondA->Excute();
else if(B is True)
commondB->Excute();
当我们需要改变满足A True条件的事件内容时,仅需要改变变量commandA指向的命令对象即可。将事件函数、命令封装成对象以实现不同条件执行映射,即为命令模式
再一次解耦
如何将硬编码逐渐编程复用性高、耦合性低的代码,这就是编程模式的工作。那么上面的例子还改进吗?我们可以看发现commondA->Excute() 这一个执行过程仍旧是一个方法,也就是说,即使我们实现了配置事件条件映射关系,但我们仍然无法配置事件的内容,特别是针对某一个执行命令的对象。
- 我们想想语文中对“命令”这一词的定义:
命令释义:上级对下级有所指示 (摘自百度百科)
命令就表示了一类相同的操作,我们作为上级在某种条件下对“某个下级”提出需要执行的内容。结果就很明显了,我们还可以把执行命令的对象从Excute()方法中解耦出来。
class Command
{
public:
Excute(Object executor);
}
class Command1 : Command
{
public:
Excute(Object executor);
}
class Command2 : Command
{
public:
Excute(Object executor);
}
Command* commondA;
Command* commondB;
//if...else可写为
if(A is True)
commondA->Excute(<命令执行对象>);
else if(B is True)
commondB->Excute(<命令执行对象>);
注意:此解耦是根据需求而定的。部分代码不需要多个命令对象,则可以将代码简化而不需要这一层解耦操作。
撤销与重做
在《游戏编程模式》此书针对命令模式的介绍的最后,提出了“撤销与重做”的功能。这就是命令模式针对功能拓展的最好例子。
相关代码不再进行详细说明,我们谈谈具体的思路。在命令模式中,我们将命令封装成一个类实例,其Execute()方法设定了玩家的操作方式,那么在类中,我们也可以编写一个Undo()方法用于还原Execute()方法执行的操作。这仅仅要求在Command类中要记录一些Execute()方法中改变的必要数据即可。
如何进行逐步的撤销呢?这时候,我们需要编写一个Command队列存储和管理Command实例。当玩家需要撤销时,Command队列进行出栈,并调用出栈实例的Undo()方法。当队列为空时,证明我们之前修改的内容已经全部撤销完毕。
总结
- 命令模式极大程度上的避免了对代码灵活部分进行硬编码。
- 命令模式本质就是将函数式事件封装成一个Command类实例,实例中实现了我们对命令的需求功能(例如对命令对象的执行、撤销命令功能等)
- 可以把命令模式看作是类形式的回调函数,将事件作为参数化并自定义条件-事件映射、执行内容。