COMMAND 模式

 

COMMAND 模式

——《敏捷软件开发 原则、模式与实践(c#版)》第21章

描述 

    COMMAND模式是最简单、最优雅的模式之一。  

     如图1-1所示,COMMAND模式简单的几乎可笑。该模式仅由一个具有唯一方法的接口组成,这似乎很荒谬?代码1-1 给出了图1-1对应的代码。

 

图1-1 COMMAND 模式

代码清单 1-1 Command.cs

   public interface Command
   {
      
void Execute();
   }

 

    事实上,该模式跨越了一条非常有趣的界限。而这个交界处正是所有有趣的负载性之所在。大多数类都是一组方法和相应的一组变量的组合。COMMAND模式不是这样的。它只是封装了一个没有任何变量的函数。

    从严格的面向对象意义上来讲,这种做法是被强烈反对的,因而它具有功能分解的味道。它把函数层面的任务提升到了类的层面。这简直是对面向对象的亵渎!然而,在这两个思维范式(Paradigm)的碰撞处,有趣的事情发生了。 

简单的 Command

   这是一个嵌入式实时软件,该软件驱动着一种新型复印机的内部工作流。在这里可以使用Command模式来控制硬件设备。如图1-2所示

 

图1-2 复印机软件中的一些简单的Command

    这些类的职责很明显。如果调用 RealyOnCommand 的Execute() 方法,它就会开启继电器。如果调用 MotorOffCommand 的 Execute() 方法,它就会关闭发动机。继电器或者发动机的地址作为构造函数的参数传到对象中去。

    有了这种结构,我们就可以在系统中传递 Command 对象并调用它们的 Execute() 方法,而无需明确的知道它们所代表的 Command 的种类。这会带来一些有趣的简化。

    该系统是事件驱动的。继电器是开还是关,发动机是启动还是停止,离合器是使用还是未使用,都取决于系统中发生的特定事件。在这些事件中,许多是通过传感器检测的。例如,当光学传感器检测到一张纸已经到了传送路径中的一个特定点时,就需要启用一个特定的离合器。那么,我们只要把合适的 ClutchOnCommand 绑定到控制那个光学传感器的对象上,就可以实现这个功能了。参加图1-3。

 

图1-3 由Sensor驱动的Command

    这个简单的结构具有一个巨大的优点。Sensor 不知道它所做的事情。每次当它检测到一个事件时,只需调用它所绑定的 Command 对象的 Execute() 方法即可。这就是说 Sensor 无需知道特有的离合器或者继电器,也无需知道纸张传送装置的机械结构,这样它们的功能就变得相当简单。

    当传感器检测到事件后,决定哪些继电器要被关闭的负载逻辑就移到了一个初始化函数中。在系统初始化的某一时刻,每个传感器都被绑定到对应的Command 对象上去。也就把 Sensor 和 Command 之间的所有逻辑互连(连接配置)放置在一个地方,并使之和系统的主体部分分离。事实上,可以创建一个简单的文本文件 来描述 Sensor 和 Command 之间的绑定关系。初始化程序可以读取该文件,并构建出对应的系统。这样,系统中的逻辑互连关系可以完全在程序以外确定,并且对它的调整也不会引起重新编译。(这一技术在 C# 中可通过 XML 和反射来解决。) 

    通过对Command(命令)这一概念的封装,该模式解除了系统的逻辑互连关系和实际连接的设备之间的耦合,这是一个巨大的好处。 

 事务

    另外一个COMMAND模式的常见用法是创建和执行事务(transaction)。例如,假想我们正在编写一个维护雇员数据库的软件(参加图1-4)。用户对数据库可以执行许多操作,比如他们可以增加雇员、删除老雇员,或是修改现有雇员的属性。

    当用户决定增加一个新雇员时,该用户必须详细指名成功创建一条雇员记录所需要的所有信息。在使用这些信息前,系统需要验证这些信息语法和语义上的正确性。COMMAND模式可以协助完成这项工作。Command 对象存储了还未验证的数据,实现了实施验证的方法,并且实现了最后执行事务的方法。

    例如,在图1-4中。AddEmployeeTransaction 类包含有和 Employee 类相同的数据字段同时它还持有一个指向 PayClassification 对象的指针。这些数据字段和对象是根据用户指示系统增加一个新雇员时指定的信息创建出来的。


图1-4 雇员数据库 和 AddEmployee 事务 

    validate 方法检查所有数据并确保数据是有意义的。它检查数据语义和语法上的正确性。它甚至会做一些确保事务操作中的数据和数据库的现有状态一致的检查。例如,它有可能要确保不存在的某一雇员。

    execute 方法用已经验证过的数据去更新数据库。在我们这个简单的例子中,AddEmployeeTransaction 对象创建新的 Employee 对象,并且初始话它的数据成员。PayClassification 对象会被已到或者复制到 Employee 对象中。

实体上的解耦和时间上的解耦

    这给我们带来的好处在于很好地解除了从用户获取数据的代码、验证并操作数据的代码以及业务对象本身之间的耦合关系。例如,可能会有人想过通过某些 GUI 中的对话框框来获取增加新雇员时需要的数据。如果 GUI 代码中包含了该事务中的验证和执行算法啊,那么就会很可惜。这样的耦合会使验证和执行代码无法在其它的接口中使用。通过把验证和执行代码分离到 AddEmployeeTransaction 类中,我们从实体上解除了该代码和获取数据的接口间的耦合关系。更甚者,我们也分离了知道如何操作数据库逻辑的代码和业务实体本身。

时间上解耦

    我们也以一种不同的方式解耦了验证和执行代码。一旦获取了数据,就没有理由要求验证和执行方法立即被调用。我们把事务对象放在一个列表中,以后再进行验证和执行。

    假设我们有个数据库必须在一条之内保持不变。对数据库的修改只能在夜里0点和1点之间进行。必须得等到午夜,难后匆匆忙忙杂字1点前把所有的命令都输入进去,这是不应该的。如果能够输入所有的命令并当场验证,难后在午夜里自动执行,就非常方便了。COMMAND模式使之成为可能。

Undo() 方法

    在图1-5中给COMMAND模式增加了Undo() 方法。显而易见,如果Command 派生类的 Execute() 方法可以记住它所执行的操作的细节,那么Undo() 方法就可以取消这些操作,并把系统恢复到原先的状态。

    假如,假想有一个允许用户在屏幕上画几何图形的应用程序。其中有一个具有一些按钮的工具条,用户可以通过这些按钮去画圆、正方形、矩形等等。用户单击了 Draw Circle(画圆)按钮,系统就创建一个 DrawCircleCommand 对象,并调用了该对象的 Execute()  方法。DrawCircleCommand 对象跟踪用户的鼠标,等待在制图窗口中的一次单击。接受到该单击时,它将这个单击点作为圆心,并且开始以当前鼠标所处的位置到圆心的距离作为半径画动态变化的圆。当用户再次点时,DrawCircleCommand 对象停止动态圆的绘制,并且把相应的圆对象加入到目前在画布上显示的图形对象的列表中。同时,它把这个新圆对象的 ID 作为自己的私有变量存储起来。接着,Execute() 方法返回。难后,系统把这个执行过的DrawCommand 对象压入已完成的命令栈中。

图1-5 COMMAND模式的Undo变体 

    随后,用户单击了工具条上的 Undo 按钮。系统弹出已完成命令栈栈顶的 Command 对象,并调用该对象的 Undo() 方法。接收到 Undo() 消息时,DrawCircleCommand 对象从当前画布上显示的对象的列表中删除和自己所保存的ID匹配的圆。

    使用这种技术,可以容易地在几乎所有的应用程序中实现Undo命令。知道如何去撤销命令的代码几乎总是和知道如何去执行该命令的代码相似。 

 

    End. 

posted @ 2009-06-18 14:09  Old  阅读(2059)  评论(5编辑  收藏  举报