《点睛简单脚本引擎》源代码导读 —— 原型、命令模式应用

这篇文章,是2004年写的,本来没准备再重发,今天因为一些事,翻了出来,鉴于现在仍然有很多人停留在YY工厂模式和单件模式上,另外,我觉得这篇文章和所涉及的代码写的还是挺不错的,所以重发出来,个人觉得,还是有意义的。

  前几天,和朋友谈论《点睛简单脚本引擎》的代码,发现,其实还是需要讲解一下,特别是说明为什么这种方式比直观的代码编写更好。

  脚本本身的格式比较简单,所有的参数用“|”分隔,第一个参数是命令名,如果命令名为空,则此行被视为“注释”。

  从类图来看代码结构就非常清晰了,其中,黄色的类是“静态类”,也就是只包含一些静态函数的类;而粉色的类是“自定义异常”;青色的类是“接口”;淡绿色的类是普通的类。我们先来看一下类图:

 

 

  其中,ApplicationEntry 是程序的入口点,其中的 Main 函数创建一个 ProcessorControlCenter 的实例,并且执行它的 RunScript 函数。而 ProcessorControlCenter 中的 RunScript 函数也很简单,只是使用了 ProcessorFactory 创建出一个 CallScriptProcessor,并且执行它的 DoProcessor 函数。

  最基本的接口是一个 IProcessor 接口,包含一个 DoProcess 函数,然后 IProcessorInstance 继承了 IProcessor,包含一个 CreateInstance 函数。其目的在于,IProcessor 接口定义出每一个 Processor 都要执行的操作,而 IProcessorInstance 则是为了让 ProcessorFactory 能够简单的创建出“处理器”。

  从 IProcessor 来理解,就非常简单、直观了。比如 CopyFileProcessor,和脚本中的“CopyFile | 源文件 | 目标文件 | [是否覆盖 = true]”相对应,所以,它包含三个属性:SourceFileName、TargetFileName 和 OverWrite。在它的构造函数中,根据传入的参数字符串数组,分析出那三个属性各自应该是什么,如果参数有错误,则抛出异常,而在 DoProcess 函数中,根据这三个参数,执行文件复制工作。

  其它的 Processor 都大同小异,不过,比较特殊的是 CallScriptProcessor。这一个 Processor 的特殊之处在于,它要读出整个脚本,并通过 ProcessorFactory 创建出相应的 Processor,并且插入它的 _ProcessorList 集合这个属性之中。在 DoProcess 函数中,它执行一个简单的遍历,顺序执行 _ProcessorList 集合中所有的 Processor 的 DoProcess 函数。

  所以,在程序运行后,首先创建出一个 CallScriptProcessor,它的 _ProcessorList 集合包含所有这个脚本中的 Processor,而如果这些 Processor 中又有 CallScriptProcessor,则这个 CallScriptProcessor 的 _ProcessorList 又包含子脚本中的 Processor,形成一个树结构,顺序遍历这颗树,并且执行其中的 DoProcess 函数,就会执行整个脚本链。这样,每一个 Processor 各司其职,把它们整合在一起后就正确的执行并得到了我们希望的结果。

  ProcessorFactory 包含一个 CreateProcessorByScriptLine 函数,它通过一个脚本行创建一个相应的 Processor。这个函数对这个脚本行用“|”分割,根据分割后的字符串数组的第一个字符串,决定创建哪一个 Processor。这个创建的过程被通过 IProcessorInstance 接口的 CreateInstance 函数分配给了各个 Processor,所以 CreateInstance 应该使用 new 来产生一个自己的实例。

  比较特殊的一点是,各个 Processor 的带参数的构造函数的访问符都是 private,这是为了防止在复制其它 Processor 代码以创建新的 Processor 的时候,忘了修改 CreateInstance 而造成错误。当其访问符是 private 的时候,如果忘了修改 CreateInstance 的话,将会产生一个编译错误,提醒我们应该修改它。

  在 ProcessorFactory 初始化的时候,通过一个 Hashtable 把脚本命令和 Processor 形成一一对应的关系,在 CreateProcessorByScriptLine 中就只要使用这个 Hashtable 就可以创建 Processor,而不必使用冗长而不易维护的 switch 语句了。

  而在第二版中,主要是增加了自定义属性 ActiveProcessorAttribute,它接受一个字符串作为 Processor 对应的脚本命令名。而在 ProcessorFactory 中,使用反射取得这个模块中所有的类,并且检查,如果某一个类是从 IProcessor 接口继承,而且定义了 ActiveProcessorAttribute 属性的话,就会被加入 Hashtable,而在 CreateProcessorByScriptLine,通过反射找到这个类的构造函数,并且使用 Invoke 调用这个构造函数,完成 Processor 的创建。这样,ProcessorFactory 在增加新的 Processor 的情况下,不需要有任何修改,而 Processor 本身,也消除了对于 CreateInstance 和无参数的构造函数的需求,变得更加简单、清晰,而 IProcessorInstance 接口也不需要了。。

  为什么说这种方法比直观的代码编写方法好呢?我们来看看一个直观的代码是怎么解决这个问题,并且对应于代码的演化,直观的代码和这种使用一个统一的接口的方式之间的差别。

  从直观的角度讲,我们的程序应该读取这个脚本,并且执行相应的操作,一个 switch 结构应该是第一个对于此类实现的想法:

 

class ApplicationEntry
{
static void Main(string [] args)
{
if ( args != null && args.Length == 1 )
{
using ( FileStream f = new FileStream(args[0]) )
{
string s;
while ( (s = f.ReadLine()) != null )
{
if ( s.Trim() != "" )
{
string ss[] = s.Split('|');
if ( ss != null && ss.Length > 0 )
{
switch( ss[0].ToLower() )
{
case "": // 是注释
break;
case "copyfile":
DoCopyFile(ss);
break;
case "deletefile":
DoDeleteFile(ss);
break;
default:
break;
}
}
}
}
}
}
}
}

 

  以上代码中没有展示 DoCopyFile 之类的函数,这些函数需要在内部解析 ss,并且执行相应的操作,在只有简单的操作,而且只有少量的指令的时候也还算可以理解,不过,增加了 CallScript 之后,就有些困难,需要进行重构了:

 

class ApplicationEntry
{
static void Main(string [] args)
{
if ( args != null && args.Length == 1 )
{
ProcessFile(args[
0]);
}
}

static void ProcessFile(string FileName)
{
using ( FileStream f = new FileStream(FileName) )
{
string s;
while ( (s = f.ReadLine()) != null )
{
if ( s.Trim() != "" )
{
string ss[] = s.Split('|');
if ( ss != null && ss.Length > 0 )
{
switch( ss[0].ToLower() )
{
case "": // 是注释
break;
case "copyfile":
DoCopyFile(ss);
break;
case "deletefile":
DoDeleteFile(ss);
break;
case "callscript":
DoCallScript(ss);
break;
default:
break;
}
}
}
}
}
}

DoCallScript(
string[] ss)
{
if ( ss.Length == 2 )
{
ProcessFile(ss[
1]);
}
}
}

  这样,这种 switch 结构使用递归调用的方法,虽然有些结构混乱,也实现了 CallScript。不过,在设计 SimpleScript 引擎的时候,有对于 Script 中的参数错误的提示的要求:

 

 

  一开始,我是把 CallScript 的 ReadToEnd( _TargetFileName ) 放在 DoProcess() 函数中的,这样,如果当前脚本出错,就会出现错误提示:

 

public void DoProcess()
{
ReadToEnd( _TargetFileName );
foreach ( IProcessor ip in _ProcessorList )
{
ip.DoProcess();
}
}

  不过,后来我觉得,这种脚本错误是应该在执行任何一个指令前进行的检查,即使对于子脚本中的错误,也应该在所有脚本执行之前提示,所以修改代码,把 ReadToEnd( _TargetFileName ) 移动到 CallScript 的构造函数中,就完成了这个任务:

 

public CallScriptProcessor(string [] Params)
{
_ProcessorList
= new System.Collections.ArrayList();
switch( Params.Length )
{
case 1:
_TargetFileName
= IOHelper.GetFullPath( Params[0] );
break;
default:
throw new ParseParamErrorException();
}
ReadToEnd( _TargetFileName );
}

  现在,我们来考虑一下上面的 switch 结构的程序要实现这种类型的脚本错误检查的话,需要进行哪些修改呢?因为在 switch 结构的程序中,脚本行的解析和执行是在同一个地方进行的,所以首先应该把它们进行拆分,不过,如果把 DoCopyFile 拆分成两个函数,那么解析函数解析出的参数怎么传递给执行函数?也许应该建立一个 CopyFile_SourceFile 和 CopyFile_TargetFile 吧。抛开参数传递的问题,如果已经把以前的 DoCopyFile 函数都拆分成了解析函数和执行函数两部分,为了实现执行前脚本参数检查,应该怎么修改程序的结构呢?恐怕还是需要一个解析用的 switch 语句组吧:

 

static void ProcessScriptParam(string FileName)
{
using ( FileStream f = new FileStream(FileName) )
{
string s;
while ( (s = f.ReadLine()) != null )
{
if ( s.Trim() != "" )
{
string ss[] = s.Split('|');
if ( ss != null && ss.Length > 0 )
{
switch( ss[0].ToLower() )
{
case "": // 是注释
break;
case "copyfile":
ParseCopyFileParam(ss);
break;
case "deletefile":
ParseDoDeleteFileParam(ss);
break;
case "callscript":
ParseDoCallScriptParam(ss);
break;
default:
break;
}
}
}
}
}
}

static void ProcessFile(string FileName)
{
using ( FileStream f = new FileStream(FileName) )
{
string s;
while ( (s = f.ReadLine()) != null )
{
if ( s.Trim() != "" )
{
string ss[] = s.Split('|');
if ( ss != null && ss.Length > 0 )
{
switch( ss[0].ToLower() )
{
case "": // 是注释
break;
case "copyfile":
ParseCopyFileParam(ss);
ProcessCopyFile(ss);
break;
case "deletefile":
ParseDoDeleteFileParam(ss);
ProcessDeleteFile(ss);
break;
case "callscript":
ParseDoCallScriptParam(ss);
ProcessCallScript(ss);
break;
default:
break;
}
}
}
}
}
}

  现在,switch 结构的程序已经变成了这种庞大而难于维护的样子,如果要增加一个新的操作,就更加困难了,除了要实现相应的解析执行函数之外,还要维护这两个庞大的 switch 结构,出错的可能性也大大增加了。而相对的,我写的 SimpleScript 的代码,任何时候新增一个操作,都是同样的简单……

  当然,对于 Processor 的结构来说,我使用的是命令(Command)模式,而对于 Processor 的创建来说,我使用的是原型(Prototype)模式。不过,在设计中,我并没有,其实也不太清楚我使用了哪种模式,只是在多种想法中进行选择,对代码进行重构,最终得到了我认为最好的结构 —— 而这种结构正好符合了某些设计模式。现在看来,反而是这种自行发现模式的情况,更有利于对于设计模式有更深的认识。

 

源代码下载:

点睛简单脚本引擎 第一版

点睛简单脚本引擎 第二版

posted @ 2010-12-23 13:40 梁利锋 阅读(...) 评论(...) 编辑 收藏