Robert C.Martin 著,邓辉译,孟岩审,清华大学出版社,2003年9月第1版
第13章 COMMAND模式和ACTIVE OBJECT模式
没有人天生就具有命令他人的权利。
——Denis Diderot(1713-1784,法国哲学家,百科全书编者)
在近几年记述过的所有设计模式中,我认为COMMAND模式是最简单、最优雅的模式之一。但是我们将会看到,这种简单性是带有欺骗性的。COMMAND模式的适用范围是非常宽广的。
如图13.1所示,COMMAND模式简单得几乎可笑。程序13.1中的代码并没有起到削弱这种印象的作用。该模式仅由一个具有惟一方法的接口组成,这似乎是荒谬的。
<<interface>> Command |
+do() |
程序13.1 Command.java
public interface Command { public void do(); }但是,事实上,该模式横过了一条非常有趣的界线。而这个交界处正是所有有趣的复杂性之所在。大多数类都是一组方法和相应的一组变量的结合。COMMAND模式不是这样的。它只是封装了一个没有任何变量的函数。
从严格的面向对象意义上来讲,这种做法是被强烈反对的——因为它具有功能分解的味道。它把函数层面的任务提升到了类的层面。这简直是对面向对象的亵渎!然而,在这两个思维范式(paradigm)的碰撞处,有趣的事情发生了。
13.1 简单的COMMAND
(略,请参看原书)13.2 事务操作
(略,请参看原书)13.3 UNDO
(略,请参看原书)13.4 ACTIVE OBJECT模式
ACTIVE OBJECT模式是我最喜欢使用COMMAND模式的地方之一。这是实现多线程控制的一项古老技术。该模式有多种使用方式,为许多工业系统提供了一个简单的多任务核心。想法很简单。考虑程序13.2和程序13.3。ActiveObjectEngine对象维护了一个Command对象的链表。用户可以向该引擎(engine)增加新的命令,或者调用run()。run()函数只是遍历链表,执行并去除每个命令。
(原书中是java程序,现翻译为C#程序,需要C#2.0编译器。根据C#的惯例,接口名称以I开头,方法名称的第一个字母大写。)
程序13.2 ActiveObjectEngine.cs















































考虑一下程序13.4中的测试用例。它创建了一个SleepCommand对象。其中,它向SleepCommand的构造函数中传了一个1000ms的延迟。接着把SleepCommand对象放入到ActiveObjectEngine中。调用run()后,它等待指定数目的毫秒。
程序13.4 TestSleepCommand.cs















































程序13.5展示了SleepCommand的实现。在执行时,SleepCommand检查自己以前是否已经执行过,如果没有,就记录下开始时间。如果没有过延迟时间,就把自己再加到ActiveObjectEngine中。如果过了延迟时间,就把wakeup命令对象加到ActiveObjectEngine中。
程序13.5 SleepCommand.cs






















































csc /t:library /out:SleepCommand.dll SleepCommand.cs ActiveObjectEngine.cs Command.cs
csc /t:library /out:TestSleepCommand.dll /r:c:\NUnit\bin\nunit.framework.dll /r:SleepCommand.dll TestSleepCommand.cs
其中 NUint 请到 http://www.nunit.org 下载。然后运行 nunit-gui.exe 的结果如下:

)
我们可以对该程序和等待一个事件的多线程程序做一个类比。当多线程程序中的一个线程等待一个事件时,它通常使用一些操作系统调用来阻塞自己直到事件发生。程序13.5中的程序并没有阻塞。相反,如果所等待的((currentTime-startTime)<sleepTime)这个事件没有发生,它只是把自己放回到ActiveObjectEngine中。
采用该技术的变体(variations)去构建多线程系统已经是并且将会一直是一个很常见的实践。这种类型的线程被称为run-to-completion任务(RTC),因为每个Command实例在下一个Command实例可以运行之前就运行完成了。RTC的名字意味着Command实例不会阻塞。
Command实例一经运行就一定得完成的特性赋予了RTC线程有趣的优点,那就是它们共享同一个运行时堆栈。和传统的多线程系统中的线程不同,不必为每个RTC线程定义或者分配各自的运行时堆栈。这在需要大量线程的内存受限系统中是一个强大的优势。
继续我们的例子,程序13.6展示一个简单的程序,其中使用了SleepCommand并展示了它的多线程行为。该程序被称为DelayedTyper。
程序13.6 DelayedTyper.cs

































































请注意DelayedTyper实现了Command接口。它的execute方法只是打印出在构造时传入的字符,检查stop标志,并在该标志没有被设置时调用delayAndRepeat。delayAndRepeat方法使用构造时传入的延迟构造了一个SleepCommand对象,再把构造后的SleepCommand对象插入ActiveObjectEngine中。
该Command对象的行为很容易预测。实际上,它维持着一个循环,在循环中重复地打印一个指定的字符并等待一个指定的延迟。当stop标志被设置时,就退出循环。
DelayedTyper的main函数创建了几个DelayedTyper的实例并把它们放入ActiveObjectEngine中,每个实例都有自己的字符和延迟。接着创建了一个SleepCommand对象,该对象会在一段时间后设置stop标志。运行该程序会打印出一个简单的由'1'、'3'、'5'以及'7'组成的字符串。再次运行该程序会打印出一个相似,但是有差别的字符串。这里是两次有代表性的运行结果 (我用我机器上的运行结果代替了原书中的运行结果):
13571113115137113151113171513111311571...
13571113151317111315131171135111311571...
这些字符串之所以有差别是因为CPU时钟和实时时钟没有完美的同步。这种不可确定的行为是多线程系统的特点。
13.5 结论
COMMAND模式的简单性掩盖了它的多功能性。COMMAND模式可以应用于多种不同的美妙用途,范围涉及数据库事务操作、设备控制、多线程核心以及GUI的do/undo管理。有人认为COMMAND模式不符合面向对象的思维范式(paradigm),因为它对函数的关注超过了类。这也许是真的,但是在实际的软件开发中,COMMAND模式是非常有用的。
参考文献
1. Gamma, et al. Design Patterns. Reading, MA: Addision-Wesley, 1995.2. Lavender, R.G., and D.C.Schmidt. Active Object: An Object Behavioral Pattern for Concurrent Programming, in "Pattern Languages of Program Design" (J.O.Coplien, J.Vlissides, and N.kerth, eds.). Reading, MA: Addision-Wesley, 1996.