级别: 初级 BenoÎt Marchal (bmarchal@pineapplesoft.com), 顾问, Pineapplesoft
2004 年 12 月 01 日 本文中,BenoÎt 继续研究 XM 的新版本,XM 是基于 XML 并与 Eclipse 集成的一种简单的内容管理解决方案。BenoÎt 讨论了代码重构中面临的问题以及如何在 Eclipse 中创建增量构造器。 请在本文附带的
讨论论坛中与作者和其他读者分享您对本文的想法。(您也可以单击本文顶部和底部的
讨论来访问论坛。)
我认为软件开发中很少有比重构代码更缺乏魅力的任务,但对于维护进行中的项目它仍然是一项重要的活动。
本专栏背后的初衷之一就是开发一个跨越数年的项目,以应对实际项目中遇到的典型挑战。重构就是这些挑战之一。
撰写本专栏的同时,我还在 Eclipse 构建过程上花了一些时间,以保证与用户界面的平滑集成,更具体地说,就是错误报告。
重构
我痛恨丢弃正在工作的代码。抱着这种观念,我尝试定期重构自己开发(或者指导,或者项目管理)的应用程序。
从经验出发
大多数软件项目的早期阶段,目标通常是让某些东西运行起来。这些东西可能并不美观,但如果能够展示某些东西,往往可以让管理人员、开发团队和用户安下心来。下一步就是通过完善特性集、修正 bug 让软件更加稳定。
但最重要(也最耗时)的阶段是维护。软件的演进主要发生在整个维护阶段。如果最初的设计引向了岔路,开发人员常常对原有代码和初始设计感到灰心丧气。事实上,我常常听到开发人员抱怨说,他们不得不扔掉去年或者上半年编写的一文不值的东西。
但是,我不喜欢扔掉正在工作的代码。这些代码包含有关于特殊情况、极端案例和非正常需求的大量有价值的经验,通常在其他地方是没有记录的。多数情况下,再次体验这些教训要比在现有基础上工作更为痛苦。
同时,我也可以理解对现有代码的抱怨。应用程序常常已经进展到这样一种境地,开发人员要花更多的时间和原始设计搏斗而不是从中受益。
面对这种情况,似乎重新编码是惟一的办法。但是我更愿意
重构已有的代码。所谓重构,我是说在那些能够从代码库获得最大好处的地方精心制作一个新的设计。工具厂商不断在其产品中结合对重构的支持(实际上 Eclipse 就有一个重构菜单,提供了很多支持工具),但这仍然是一个需要手工操作的过程。下面就是我采用的方法。
重构 XM
上一期文章中曾经提到,XM 已经存在很长一段时间了。该项目于
2001 年 7 月在
使用 XML 专栏开创。它现在已经支持 Eclipse 和不同项目中的许多特殊需要。
因此,至少有四个代码库可以看作是 XM:最初的 XM 代码(发布在 ananas.org 上)和基于不同设计的三套功能类似(但略有差异)的代码。这个练习的目标就是统一不同的版本。
上一期文章中还讨论了我计划如何改进现有的设计,以便从文件系统中抽象出 XM。
实际上,重构通常包括以下活动:
- 将大的单元(包、类或接口)分解成较小的单元
- 在新接口背后抽象出更多的概念
- 对概念进行重新命名,以便更好地表达其用法
此外,该过程还可能包括合并那些经验表明非常类似的概念。比如,我发现 Messenger 混合了两种不同的概念:向用户报告信息和向本地化报告信息,因此我设计了两个新的接口(
MessageListener 和
ResourceHolder),让新的 Messenger 继承这两个接口。
我尽量避免编写新的代码。我倾向于尽可能重用原来的代码,以便减少这个过程可能引入的 bug 数量。如果确实需要编写新的代码(为了支持新的概念,比如新的抽象),我将其隔离在单独的类中。
我发现更可行的办法是在 Eclipse 中创建一个新项目,然后将分析阶段认为稳定的现有类移到这个项目中。我使用 Eclipse 的重构特性(比如传播包的变化)来重新命名其中的一些类(参见图 1)。
图 1. Eclipse 中的重构帮助
然后把注意力放到需要改变的类上,如 Messenger。我把原有的文件复制到新项目中,通过剪切和粘贴来按照预期的目标重新组织。
复制代码时经常会发现预料之外的依赖性。编译器是最好的伙伴,在解决依赖性问题之前它会提示错误。通常这意味着要进一步打破单元,进一步抽象和更多的重命名。
实践中的问题
从现有代码中发现宝贝,然后围绕着它们重新组织需要花费时间,但是要知道,按照计划(XM 做得不太好,在此我要向 developerWorks 编辑 John 致歉)重新编写经过彻底调试的、稳定的代码要花费更多的时间。
重构是达到目标的一种方法。在沉闷的会话中间,它会提醒您目标是设计的演化,从而为应用程序增加新的特性。我发现,如果有很多好的乐曲,打开 iTunes 工作的同时享受音乐也很有益处。
重构的范围可以是一个类,也可以是整个项目。就我们这个例子而言,修改的地方很多,而且项目不大,因此我选择了启动一个新的项目。对于更大的项目,可以每次重构一个包,但过程是一样的:将原有的代码移到项目之外,然后再逐渐地重新引入。
什么时候要重构呢?最理想的情况是,一旦发现围绕着现有设计所花费的时间超过利用它的时间就开始重构。
Eclipse 构建过程
上一期文章中已经讨论了 Eclipse 构建过程(请参阅“
使用 XML:用 Eclipse 和 XM 构建项目”),但当时我忽略了 Eclipse 资源管理。本期文章重新探讨构建过程,并实现了一个功能递增的构造器。
项目描述
构建项目从项目描述中的构建规范开始。项目描述文件(.project)在项目文件夹中。(因为其文件名格式为“.something”,在 UNIX 中是不可见的)。清单 1 给出了相关的片段。
清单 1. 项目描述片段
<buildSpec>
<buildCommand>
<name>org.ananas.xm.eclipse.builder</name>
<arguments></arguments>
</buildCommand>
</buildSpec>
|
<buildSpec> 标签中包含一组构造器(和可选的参数)。用户可以通过
File|Properties 下的
Properties 对话框编辑项目描述。
多数插件还提供了一个更方便的编辑
buildSpec 的接口。比如,您可能还记得最初 XM 插件使用向导和定制的配置面板配置
buildSpec(请参阅以前的专栏文章“
使用 XML:Eclipse 中的布局、属性和首选项”)。
构造器声明
buildSpec 中的名字指向 plugin.xml 文件中声明的扩展点。构造器扩展了
org.eclipse.core.resources.builders ,并将其与一个类关联(请参阅清单 2 中的
org.ananas.xm.eclipse.XMBuilder)。
清单 2. 插件声明片段
<extension id="builder"
name="XM Builder"
point="org.eclipse.core.resources.builders">
<builder>
<run class="org.ananas.xm.eclipse.XMBuilder"/>
</builder>
</extension>
|
类
builder 继承自
org.eclipse.core.resources.IncrementalProjectBuilder 并实现了
build() 方法。
 |
扩展点和类
忠告:Eclipse 中很容易混淆扩展点和类。虽然看起来相似,但这是两个不同的概念。
扩展点出现插件描述中(plugin.xml 文件),它规定了插件如何扩展 Eclipse 平台。每个扩展点都有唯一的标识符。
之所以容易混淆,是因为扩展点标识符也使用域名逆序构造,与包名类似。扩展点的例子有
org.eclipse.core.resources.builders 和
org.eclipse.ui.propertyPages。
因此,您可能认为扩展点是一个 Java 类或者 Java 接口。扩展点是一个完全抽象的概念。(可以将扩展点看作插件能够连接的端口。)
插件描述符包含足够的信息,这样平台直到最后一刻之前都无需装载 Java 类。比如,平台只需要查看描述符就能增加菜单项。只有当用户选择这个菜单项时,才需要加载相应的类。这样有助于改善性能,因为任何时候平台都只加载最少的代码。扩展点可以满足这种要求。
当然,扩展点和 Java 类之间也存在联系:多数扩展点都是通过插件中的某个 Java 类实现的。类必须为扩展点实现适当的接口。
|
|
构造器实现
构造器实现如
清单 3 所示。它继承
IncrementalProjectBuilder 并实现了
build() 方法。
build() 有三个参数:
- 请求的构建类型——增量、自动还是完全
- 构件参数映射
- 进程监控器——实际上就是用户界面中的进度条
增量构建和自动构建非常相似,惟一区别就是增量构建需要用户明确要求,而自动构建在用户保存文件时自动启动。(增量构建或自动构建可以从 Project 菜单中选择。)
Eclipse 记录资源的变化并向构造器传递一个
delta,即上一次构建以来变化的资源列表。注意,Eclipse 不保证一定能传递 delta。为了节约内存,Eclipse 可能随时抛弃 delta。如果没有可用的 delta,构造器必须执行完全构建。
该方法返回一个项目列表,构造器希望下一次运行时能使用这些项目的 delta。对于跨越多个项目的构造器,这样可以提高增量构建的速度。
build() 首先检查项目是否已经打开,能不能访问,然后测试构建的性质。实际上,它并不区分增量构建和自动构建。
访问器
为了处理资源,构造器使用了
访问器模式。访问器模式在
Design Patterns(设计模式) (请参阅
参考资料)一书中进行了详细讲解。访问器模式实现了层次结构的遍历,并将访问器指定的操作应用于每个节点。但是,访问器不需要知道节点的层次结构。
访问器模式如图 2 所示。节点实现了
accept() 方法,该方法接受访问器作为参数。
accept() 直接对访问器调用
visit() 方法,并把节点作为参数传递。在调用
visit() 的过程中,访问器对节点执行适当的操作。最后,节点循环遍历所有的后代,并使用访问器调用它们的
accept() 方法。这样就能保证访问器访问了整个层次树。
图 2. 访问器模式
构造器必须实现两个访问器:
IResourceDeltaVisitor 和
IResourceVisitor,分别用于实现增量构建和完全构建。在 XMBuilder 中,访问器通过单个的内部类
Visitor 实现。
delta 访问器的实现(方法
visit(IResourceDelta))过滤掉删除部分。如果调用了相应的新资源或者已有资源的变动,则调用
visit(IResource)。
visit(IResource) 方法实现了实际的构建逻辑。一般而言,该方法非常简单,就是调用编译器或解释器。对于 XM,
BatchSupervisor 扮演了编译器的角色。(如果曾经用过原来的 XM,
BatchSupervisor 类似于老代码中的
MoverSupervisor)。在目前的实现中,
BatchSupervisor 将样式表用于资源。
要注意,在调用编译器之前,访问器要测试资源的两个标志:影子(phantom)和团队私有(team private)。影子资源是临时文件,团队私有资源是对构造器不可见的配置文件。比如,版本控制(CVS 或 Subversion)将其配置文件标识为团队私有。事实上,您也不希望构造器设置版本控制数据的样式。
谈到了标志,访问器也会对目标文件夹(存放输出文件)设置派生文件夹的标志。这样就告诉版本控制系统该文件夹是从项目中的其他文件生成的。因此,它还可能重新生成,不必对其进行版本控制,从减少了存档的数量。
报告错误
上一期文章中讨论了如何通过对资源附加标记在任务列表中报告错误。这样做需要能够正确地识别资源。问题是构造器需要调用编译器或者(该例中)XSLT 解释器这样的工具,这类工具不使用 Eclipse 资源对象,而使用系统标识符(URL)或者 Java File 对象。清单 4 说明了如何从文件路径重新加载资源。
清单 4. 插件声明片段
public static Filename createFilenameFromSystemID(String systemID)
{
IPath path = new Path(systemID);
IResource resource = workspaceRoot.getFileForLocation(path);
if(null == resource)
resource = workspaceRoot.getContainerForLocation(path);
if(null != resource)
return new EclipseFilename(resource);
else
return null;
}
|
结束语
使用 XML 专栏的初衷之一是开发一个较大的项目,从而通过一系列文章阐述现实生活中的问题,比如重构。我希望我已经让您认识到了现有代码库的价值,下一次要推倒重来的时候会考虑到重构。
参考资料
关于作者
对本文的评价
|