设计模式 学习笔记 之六
第8章 Command模式
在近几年记述过的所有设计模式中,Command模式是最简单、最优雅的模式之一。Command模式的适用范围很广,我们首先通过一个实例来学习它的最主要的一个应用场合:对用户的操作建模。
例子:处理雇员信息数据库系统的用户命令
假设我们在开发一个雇员信息数据库系统。该系统的1.0版本采用的是命令行界面。下面给出几个常用的用户命令。
加入新的雇员 | % add_emp ID id NAME name GENDER gender BIRTHDAY yyyy-MM-dd AGE age |
按ID号删除雇员 | % del_emp ID id |
更新雇员信息 | % update_emp ID id NAME new_name AGE new_age |
我们现在需要设计一个方案,来对用户命令进行处理,这里的处理包括命令的验证和执行。我们希望在这个设计中,用户界面相关的代码与用户命令处理的代码是分离的,这是因为用户界面往往是在不断变化的,1.0版本使用命令行界面,2.0版本可能使用Windows GUI界面,3.0版本则可能使用其他跨平台GUI界面,因此把用户界面代码与业务执行逻辑相分离总是一个好的想法。
有了将用户界面与命令处理相分离的基本想法,我们可以得到这样一种设计:用户界面代码放入一个命令解析器中,而命令的验证和执行逻辑则封装到单独的函数中。借助数据驱动法,我们可以用下面的代码来表示这个面向过程的设计:
String command = getCommand(userInptu);
Map<String, String> parameters = parseParameters(userInput);
if (validate[command](parameters))
{
execute[command](parameters);
}
这个设计由于使用了数据驱动法,因此看上去还不坏。但是,它还是缺少了面向对象设计的风格。之所以这样,是因为它只是把用户命令的验证和执行逻辑封装到函数中,而不是class中。我们现在来试试用class来表示用户操作,从而得到下面的设计。
有了这样的class hierarchy,我们可以把上面的代码重写为
String commandName = getCommandName(userInput);
Map<String, String> parameters = parseParameters(userInput);
Command command = getCommand(commandName, parameters);
if (command.validate())
{
command.execute();
}
这样的设计正是Command模式的应用。Command模式的核心是建议用一个Command类来对用户操作建模。这个Command类封装了命令验证逻辑和命令执行逻辑。这样做带来的好处是让用户操作与用户界面解耦,整个系统成为一个松散耦合的系统。
我们现在可以想一想:如果2.0版本中GUI界面替代了命令行界面,会发生什么?现在用户的输入可能是通过对话框来获取,但是,用户操作的验证和执行逻辑不会受到影响,它们仍然是被封装在Command类。
在GUI程序中,还有另一个常用的UI元素就是菜单项。当用户选择一个菜单项时,一项对应的动作就要执行,其实这里也是Command模式的应用,也即下图所表示的设计。
由此我们再思考一下:所谓的将用户操作与用户界面解耦,其实背后的根本想法是将“用户操作的触发”与“用户操作的执行”解耦。这里的解耦有两层含义:一是实体上的解耦,即“用户操作触发者”与“用户操作”是两个不相关的实体,用户操作触发者负责的是确定操作触发时间点,但它并不关心它触发的到底是什么用户操作,而用户操作关心的是自己要做些什么动作,而并不关心自己是被谁触发以及在什么时间点上触发;二是时间上的解耦,即用户操作的触发时间点与用户操作真正执行的时间点之间是不相关的。例如,用户可以在某天的中午12:00触发一条命令,但是这条命令不会马上执行,它会被放入一个命令队列中,而等到晚上12:00的时候才被执行。
建模用户操作是Command模式的一个最主要的应用场合,但是,Command模式也可以用于建模系统自发的动作或操作,我们再来看下一个例子。
例子:设备控制
假设我们现在要为复印机开发控制程序。复印机系统是事件驱动的系统,而这些事件又是通过传感器来检测的。传感器检测到某个事件发生时,就要控制硬件设备作出反应。例如:当光学传感器检测到一张纸已经到了传送路径上的一个特定点时,就要启用一个特定的离合器。我们希望对传感器及其关联的设备控制动作进行建模。
怎么来对这种设备控制问题建模呢?我们发现,这个问题其实与前面已经讲到的菜单项触发动作是有一点类似的。在复印机系统中,传感器就好比是GUI程序中的菜单项,当它发现它所检测的事件发生了时,一个对应的动作就要执行。因此我们有了如下类似的模型。
有了这样的基本思路,我们可以得到一个更具体一点的设计图。
同前面讲到菜单项的例子一样,这样设计的好处也是松散耦合,包括实体的解耦和时间的解耦。但是这里我们还要做更多一点思考:一个MenuItem是与一个Action绑定的,一个Sensor也是与一个Action绑定的,我们怎么在代码中描述这种触发者与被触发动作之间的绑定关系?最原始的办法是把这种绑定关系硬编码到代码中。但是别忘了我们还有数据驱动法。我们可以给每个MenuItem或每个Sensor分配一个ID作为索引,来查询该MenuItem或Sensor所绑定的动作。我们甚至可以把这种绑定关系写到配置文件中,这样,通过修改配置文件,我们就可以使MenuItem或Sensor绑定到不同动作上,从而改变软件的行为。这很好,不是吗?
命令的undo
许多应用程序都提供一种实用的功能:命令撤消,把之前执行过的一条或多条命令撤消掉。这种情况可以看作是对前面所讲的Command模式的增强,即在Command类中新增一个undo()方法,用于撤消该命令所带来的影响。
如果要求Command类支持撤消功能,那么就要求execute()方法能够记录下它的执行细节。这样,当undo()方法被调时,undo()就能取消这些操作,使系统回到原先的状态,并且清除掉由execute()所记录的执行细节。
当然,仅仅是在Command类中引入undo()方法是不够的。系统还需要引入一个命令栈。当用户执行一个操作时,该操作对应的Command对象的execute()方法被调,之后Command对象被压入命令栈中。当用户要求撤消操作时,命令栈顶部的Command对象被弹出,并调用其undo()方法。
Java Swing中的Undo Manager就提供了命令撤消这样的功能。我们通过下面的UML图可以大致看出它的设计思路。
小结
让我们再回顾一下本章中学习的内容。
- Command模式建议把用户操作或系统动作建模成Command类,这样做的好处是带来松散耦合,这体现在两方面:一是实体的解耦,操作触发者负责确定触发操作的时机,而Command类负责操作的验证和执行,这种解耦使得操作触发者可以灵活地与不同的操作绑定;二是时间的解耦,操作触发的时间、操作验证的时间与操作执行的时间三者互不相关。
- Command模式可以在很多场合下应用,例如建模用户命令,建模硬件控制动作,命令撤消功能等。
- 应该学会识别出Command模式。Command类一般被命名成Command,Operation或Action,而且其中的方法一般命名为do(),execute(),run(),act(),perform(),undo()和redo()等。
- 实现Command模式时常需要考虑的问题是:1. 如何把操作执行的结果返回给触发者。2. 是否支持undo功能。