银河

SKYIV STUDIO

  博客园 :: 首页 :: 博问 :: 闪存 :: :: :: 订阅 订阅 :: 管理 ::
《敏捷软件开发——原则、模式与实践》
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模式

程序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
using System.Collections.Generic;

namespace Asd.Chap13
{
  
public class ActiveObjectEngine
  
{
    Queue
<ICommand> itsCommands = new Queue<ICommand>();
    
    
public void AddCommand(ICommand c)
    
{
      itsCommands.Enqueue(c);
    }

    
    
public void Run()
    
{
      
while (itsCommands.Count > 0) itsCommands.Dequeue().Execute();
    }

  }

}

程序13.3 Command.cs
namespace Asd.Chap13
{
  
public interface ICommand
  
{
    
void Execute();
  }


  
public static class Pub
  
{
    
public static long TicksPerMillisecond = 10000;
  }

}

  这似乎没有给人太深刻的印象。但是想象一下如果链表中一个Command对象会克隆自己并把克隆对象放到链表的尾部,会发生什么呢?这个链表永远不会为空,run()函数永远不会返回。
  考虑一下程序13.4中的测试用例。它创建了一个SleepCommand对象。其中,它向SleepCommand的构造函数中传了一个1000ms的延迟。接着把SleepCommand对象放入到ActiveObjectEngine中。调用run()后,它等待指定数目的毫秒。
程序13.4 TestSleepCommand.cs
using System;
using NUnit.Framework;
using Asd.Chap13;

namespace NUnit.Tests.Asd.Chap13
{
  [TestFixture]
  
public class SleepCommandTest
  
{
    
class WakeupCommand : ICommand
    
{
      
public void Execute()
      
{
        commandExecuted 
= true;
      }

    }

    
    
static bool commandExecuted;
    
    [Test]
    
public void Sleep()
    
{
      ICommand wakeup 
= new WakeupCommand();
      ActiveObjectEngine e 
= new ActiveObjectEngine();
      SleepCommand c 
= new SleepCommand(1000, e, wakeup);
      e.AddCommand(c);
      
long start = DateTime.Now.Ticks / Pub.TicksPerMillisecond;
      e.Run();
      
long stop = DateTime.Now.Ticks / Pub.TicksPerMillisecond;
      
long sleepTime = stop - start;
      Assert.IsTrue(sleepTime 
> 1000"SleepTime " + sleepTime.ToString() + " expected > 1000");
      Assert.IsTrue(sleepTime 
< 1100"SleepTime " + sleepTime.ToString() + " expected < 1100");
      Assert.IsTrue(commandExecuted, 
"Command Executed");
    }

  }

}

  我们来仔细看看这个测试用例。SleepCommand的构造函数有3个参数。第一个是延迟的毫秒数。第二个是在其中运行该命令的ActiveObjectEngine对象。最后一个是名为wakeup的另一个命令对象。测试的意图是SleepCommand会等待指定数目的毫秒,然后执行wakeup命令。
  程序13.5展示了SleepCommand的实现。在执行时,SleepCommand检查自己以前是否已经执行过,如果没有,就记录下开始时间。如果没有过延迟时间,就把自己再加到ActiveObjectEngine中。如果过了延迟时间,就把wakeup命令对象加到ActiveObjectEngine中。
程序13.5 SleepCommand.cs
using System;

namespace Asd.Chap13
{
  
public class SleepCommand : ICommand
  
{
    ICommand wakeupCommand;
    ActiveObjectEngine engine;
    
long sleepTime;
    
long startTime;
    
bool started;
    
    
public SleepCommand(long milliseconds, ActiveObjectEngine e, ICommand wakeupCommand)
    
{
      sleepTime 
= milliseconds;
      engine 
= e;
      
this.wakeupCommand = wakeupCommand;
    }

    
    
public void Execute()
    
{
      
long currentTime = DateTime.Now.Ticks / Pub.TicksPerMillisecond;
      
if (!started)
      
{
        started 
= true;
        startTime 
= currentTime;
        engine.AddCommand(
this);
      }

      
else if ((currentTime - startTime) < sleepTime)
      
{
        engine.AddCommand(
this);
      }

      
else
      
{
        engine.AddCommand(wakeupCommand);
      }

    }

  }

}

  (请使用如下的命令编译以上程序:
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
using System;

namespace Asd.Chap13
{
  
public class DelayedTyper : ICommand
  
{
    
class StopCommand : ICommand
    
{
      
public void Execute()
      
{
        stop 
= true;
      }

    }


    
long itsDelay;
    
char itsChar;
    
static ActiveObjectEngine engine = new ActiveObjectEngine();
    
static bool stop = false;

    
static void Main()
    
{
      engine.AddCommand(
new DelayedTyper(100'1'));
      engine.AddCommand(
new DelayedTyper(300'3'));
      engine.AddCommand(
new DelayedTyper(500'5'));
      engine.AddCommand(
new DelayedTyper(700'7'));
      ICommand stopCommand 
= new StopCommand();
      engine.AddCommand(
new SleepCommand(20000, engine, stopCommand));
      engine.Run();
    }

    
    
public DelayedTyper(long delay, char c)
    
{
      itsDelay 
= delay;
      itsChar 
= c;
    }

    
    
public void Execute()
    
{
      Console.Write(itsChar);
      
if (!stop) DelayAndRepeat();
    }

    
    
void DelayAndRepeat()
    
{
      engine.AddCommand(
new SleepCommand(itsDelay, engine, this));
    }

  }

}

  (编译:csc /t:exe /out:DelayedTyper.exe /r:SleepCommand.dll 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.
posted on 2005-10-07 22:36  银河  阅读(4318)  评论(11编辑  收藏  举报