咱公司的产品几乎都是有一个核心版本,然后根据各个城市的特有的需求而作出修改和扩展,特别是互动平台,现在已经有hudongpingtai_jdz、hudongpingtai_wuxi、hudongpingtai_changzhou、hudongpingtai_jiujiang等分支的版本了。每支更新cvs的时候,都要将hodongpingtai和各城市的分支更新到本地,然后合并到另一个项目里进行开发,提交的时候又要将修改的文件从这个项目里抽取出来,分别放到对应的cvs里。
我一直觉得合并项目并不是一件有趣的事,这并不是简单的复制粘帖就能完事的,有些文件几乎是从来不参与合并的,例如ini.xml(全局配置文件),com.properties(数据库等配置),每次合并的时候都要把这些文件先删掉,以免错误地覆盖掉原来的文件。于是我就打算做一个小工具,可以对多个项目进行合并,并且可以自由输入不参与合并的文件。就这样,“智晟项目合并器”诞生了。
顾名思义,“智晟项目合并器”,只是用来合并项目的。我也只是打开vs2003,往form里拖放了几个控件,用一个listbox将所有的项目的名称显示了出来,然后在旁边也放了一个listbox,这里是显示要合并的项目的,哦,还要再拖放一个listbox,显示不参与合并的文件。数据结构也很简单,两个hashtable,分别用来存放项目的名称和项目完整路径的配对,为了防止用户输入重复的不参与合并的文件名,当然是使用set了。不过.net没有内置的set实现,那就用hashtable模拟实现了一个。没有任何架构,.net的控件事件比我编写的代码还要多,我只是写了一个doCopy的核心方法,传入源项目的路径和要合并到的项目的路径,然后递归调用自身复制文件。以下是这个方法的原型:
Public void DoCopy(string toProject, DirectoryInfo directory)
“智晟项目合并器”面世后,互动寻宝活动的开发刚刚开始。我发现了分离项目远远比合并项目繁琐多了,这不是吗?要记住修改了那个文件,还要记住这个文件是属于核心版本的还是属于城市分支版本的。有时候每天下来记事本上密密麻麻的记满了文件的完整路径。大家都在不断埋怨提交太麻烦了。我就想到既然项目有合并和分离的行为,那么我的“智晟项目合并器”也应该增加分离项目的功能了。
要升级“智晟项目合并器”的话,现在的原始的架构当然不符合新的需求了,总不能在每个控件的事件处理方法里密密麻麻的写满代码吧。下面与大家分享一下重构与改进的过程。
既然面向对象是对现实世界的抽象,很自然的,一个名为BaseProjectOperator的基类产生了。我将原来的控件事件处理方法里的与.net的控件无关的代码都放到了这个类里。例如:工作台的主目录路径,各种数据结构,DoCopy方法。然后从BaseProjectOperator里派生出一个类: ProjectMerger,这里类里有几个核心方法:
/// <summary>
/// 操作源项目的hashtable,移动到源项目列表里
/// </summary>
public void MoveToOriginal(string item);
/// <summary>
/// 操作要合并的项目的hashtable,移动到要合并的项目列表里
/// </summary>
public void MoveToMeger(string item);
/// <summary>
/// 合并项目
/// </summary>
/// <param name="toProject"></param>
/// <param name="projectQueue"></param>
public void DoMerge(string toProject, ref Queue projectQueue);
MoveToOriginal方法和MoveToMeger方法没什么好说的,只是操作对应的hashtable。而DoMerge方法则将要合并的项目放进一个队列里,然后按FIFO的规则依次调用BaseProjectOperator类的DoCopy方法进行文件复制。怎么处理不合并的文件呢,一开始也没想太多,简单的在DoMerge方法里每次复制文件前迭代一下不需要合并的文件的HashSet,如果发现不需要合并的文件就进入下一循环。
XP的创始人Kent Back在《极限编程》一书里说过极限编程的其中一个步骤:为了使程序尽快能工作,可以使用复制粘帖的方式。于是一个跟ProjectMerger类没多大分别的ProjectSplitter类产生了。
ProjectSplitter的职责是用来分离项目,里面有一个核心的方法:
/// <summary>
/// 分离项目文件
/// </summary>
/// <param name="fromProject">源项目</param>
/// <param name="toProject">要分离到的目标项目</param>
/// <param name="day">分离多少天前的文件</param>
public void DoSplit(string fromProject, string toProject, int day);
此方法判断到底该分离多少天前的文件,然后调用BaseProjectOperator类的DoCopy方法复制文件。
这样的继承关系没有什么问题,不过在实现上太生硬了,而且不利于扩展,子类里充斥里判断着否需要进行合并或分离的代码。想到java里有一个FilenameFilter的接口,可以用来过滤文件,理论上.net也应该有这样的实现,但也没什么必要一定要使用.net的接口,不过.net的文件不像java那样用一个java.io.File就可以标识文件和目录,而是有专门的文件和目录类。
重构改进后的BaseProjectOperator类的核心方法如下:
/// <summary>
/// 复制文件
/// </summary>
/// <param name="toProject">目标文件</param>
/// <param name="directory">源目录</param>
/// <param name="fileFilter">文件过滤器</param>
/// <param name="dirFilter">目标过滤器</param>
protected void DoCopy(string toProject, DirectoryInfo directory, FileFilter fileFilter, DirectoryFilter dirFilter) ;
FileFilter和DirectoryFilter分别只有一个公开的方法:
/// <summary>
/// 文件过滤器
/// </summary>
public interface FileFilter {
/// <summary>
/// 是否符合要求
/// </summary>
/// <param name="file">目标文件</param>
/// <returns></returns>
bool accept(FileInfo file);
}
/// <summary>
/// 目录过滤器
/// </summary>
public interface DirectoryFilter {
/// <summary>
/// 是否符合要求
/// </summary>
/// <param name="directory">目标目录</param>
/// <returns></returns>
bool accept(DirectoryInfo directory);
}
到了这里,如果大家对java的java.io.File类的listFiles(FilenameFilter filter)有点印象的话,估计都明白了,BaseProjectOperator通过这两个接口对子类施加了策略,DoCopy方法根据FileFilter和DirectoryFilter接口的accept的返回值来判断是否复制文件,但怎么判断就要由子类去实现了。这有点像GOF设计模式中的模板方法,可以说是策略模式和模板方法的结合体。到了这里,真是觉得可笑,策略模式和模板方式本来是南辕北辙,现在却结合在一起为我服务,这也是我根本没想到的,不过一切发生的很自然,没有违反单一职责原则,也没有违反高内聚,低耦合的原则。