在 Eclipse 中创建新的重构功能

对重构的强大支持是软件开发人员喜爱 Eclipse 的一个最为重要的原因。而 Eclipse 还有一个至少和重构不相上下的优点,那就是其近乎无懈可击的可扩展性。这两者的结合意味着我们可以根据自己的需要来创建展新的重构功能。

志 甘 (ganzhi@cn.ibm.com), IBM 中国软件实验室(CSDL BJ)SOA Design Center成员

甘志,IBM 中国软件实验室(CSDL BJ)SOA Design Center成员,主要研究方向为AOP、SOA和Security,他还对羽毛球运动很感兴趣。他在上海交通大学计算机系攻读网络安全方向博士学位,期间发表了多篇论文和技术书籍。你可以通过ganzhi@cn.ibm.com联系他。



春 郭迎 (guoyingc@cn.ibm.com), IBM 中国软件实验室(CSDL BJ)的成员

郭迎春,IBM 中国软件实验室(CSDL BJ)的成员,有多年Java开发经验,目前主要研究方向为AOP、SOA。你可以通过guoyingc@cn.ibm.com联系他。



林 刘岳, IBM 中国软件实验室(CSDL BJ)成员

刘岳林,IBM 中国软件实验室(CSDL BJ)成员,在OOAD, RUP, XP, Architecture/Design Pattern方面有着丰富的项目实践经验,主要技术方向为J2EE, SOA, Grid, AOP ,PKI。



2005 年 11 月 01 日

1 介绍

重构在现代软件开发过程中扮演着重要的角色,它能够减轻软件开发人员的工作负担,提高软件开发的生产效率。为了阐明重构的重要性,我们在这里引用了 developerWorks 上 David Carew 提供的关于重构的教程中的一段话:

现在,一个开发者的工作大部分在于对现有的代码进行修改,而不是起草写新的代码。简单的修改可能包括对现有代码进行添加。然而,多样化的修改或扩展的改变会使软件内部结构开始恶化。重构改变软件的内部结构使得软件更容易理解并且在不需要改变其显著的行为的情况下使得修改的代价也更小。

在 Java 软件开发过程中,通过使用 Eclipse 提供的重构工具,我们至少获得了以下好处:

1. 最终产品更为健壮:我们对程序代码的修改将不太可能出错,出现遗漏修改的可能变少,即使出现问题也能够通过 Undo 功能回退到重构前的状态。

2. 提高了生产效率。通常一次重构能够完成对程序代码的多处改动。最为明显的例子可能是 Eclipse 提供的 Rename 重构,它能够在修改名称的同时相应的更改所有的引用。

Eclipse 为我们提供了多种实用的重构功能,在软件开发过程中使用这些重构能够给我们带来极大的好处。然而,针对每个开发人员的特殊需要,总有一些迫切需要的功能是不能通过已有的重构来获得的。这个时候,我们可以对 Eclipse 平台进行一些扩展,创建适应我们自己需要的重构。如果这个重构恰好能够符合大多数人的需要,我们也可以像其他 Eclipse 的 contributor 一样,将我们的重构贡献给 Eclipse 社区。

接下来,我们将通过一个例子来展示如何在 Eclipse 中创建新的重构功能。我们这里创建的重构将用于迁移 JUnit 的测试用例。我们知道,在当前版本的 JUnit 中,一个用于测试的函数必须以字符串"test"作为方法名称的开始。而在即将来到的 JUnit 4 中,一个"@Test"的 Annotation 被用于标明方法是一个测试方法。我们将要创建的重构将完成这个迁移工作,即在所有的以"test"开始的方法之前加上"@Test"标记。@Test Annotation 还可以包含一个 timeout 属性用来规定方法的最大执行时间,我们在向导中提供了一个页面供用户选择是否需要 timeout 属性。


2 结果预览

为了给读者一个直观的感受,我们下面首先介绍本文中例子的实际运行效果。在阅读完本文之后,读者朋友也能够顺利的完成类似的功能。

启动例子程序提供的 Refactor 之后,我们获得了一个由三个页面组成的向导。在第一个页面中,用户可以选择是否需要 timeout 参数,并且用户能够设置 timeout 参数的值。

图 1 输入参数
图 1 输入参数

当用户输入参数完毕之后,通过单击 Next 按钮我们将进入下一个页面。向导将进行初始条件检查和最终条件检查,并将检查的结果反馈给用户。在图 2 中我们可以看到,初始条件和最终条件都正常,因此我们可以进入下一步。

图 2 显示条件检查
图 2 显示条件检查

接下来是预览窗口(图 3),向导用直观的界面显示了在应用向导之后,我们将会对源代码造成怎样的改动。用户可以在这个页面中判断最终的修改是否符合自己的需要。另外,用户也能够选择性的取消对某些文件的修改。

当用户检查预览页面确认没有问题之后,用户可以按下 Finish 按钮从而完成重构。这个时候,源代码会发生修改,最后的结果如下所示:

清单 1
 package main; 
 public class TestSomething { 
	 @Test(timeout=500) 
	 public void testSomething(){ 
	
	 } 
 }

3 总体结构和流程

在 Eclipse 中,一个重构操作主要由以下三个部分组成:

1. RefactoringWizard 类:RefactoringWizard 提供了向导式的用户界面来引导用户完成重构工作。不需要我们做任何工作,Eclipse 已经通过 RefactoringWizard 为我们提供了预览页面、条件检查页面以及 Undo/Redo 等功能。我们需要继承这个类从而为重构过程提供特定的用户界面。

2. Refactoring 类:Refactoring 类完成具体的定位和修改代码功能。为了建立新的 Refactoring,我们需要继承这个类并实现重构的逻辑部分。

3. AST 和 ASTParser:在 Refactoring 类中,我们需要对代码进行定位和修改,这可以通过 AST 机制来完成。AST 是 abstract syntax tree 的简称,它能够将 Java 代码解析成为一个树形结构。在利用了 AST 树之后,对源代码的修改变成了对 AST 树的遍历、更改节点属性,以及插入和删除节点等。

一个典型的重构操作流程如下所示:

1. 用户选择要进行重构的对象,通过菜单项或按钮启动重构操作。

2. 创建具体的 Refactoring 类,弹出 RefactoringWizard。

3. RefactoringWizard 与用户交互,引导用户输入必要的参数;RefactoringWizard 调用 Refactoring 类的函数进行条件检查。

4. Refactoring 类创建 AST,并利用其对源代码进行定位和修改。这里进行的修改并不直接应用到源代码上,而是被保存成 Change 对象,供 Refactoring 框架使用。

5. RefactoringWizard 调用 Refactoring 类的函数,获得重构内容的详细描述信息(即第 4 步生成的 Change 对象),显示在预览界面上,待用户确认。

6. 用户确认后 Refactoring 框架将修改代码,重构操作结束。

接下来,我们将详细介绍新建重构类型的各个步骤。


4 创建插件工程

在大家对整个系统构架有了一个大概的了解之后,我们的介绍就从创建工程开始。大家都知道 Eclipse 提供了很好的扩展性,通过创建插件就能把我们要添加的重构功能无缝的插入到 Eclipse 平台中。创建插件工程的方法在很多地方都有介绍,这里不再详细讲解。如果需要基础的插件开发知识,我们可以参考《 开发 Eclipse 插件》,树立基本的插件开发意识。

通过菜单 File -> New-> Project,选择 Plug-in Project。点击 Next,出现对话框,输入项目名称 manage.annotation,接受其他选项的默认值。点击 Next,出现插件属性设置的对话框,继续接受默认值。点击 Next,出现选择插件模板对话框,该工程要在 Refactor 菜单中添加一个新的菜单项,所以这里我们采用"Hello,World"的插件模板。点击 Next,修改"Action 类名称"的值为 AnnotationManageAction,点击 Finish 按钮。至此,一个最基本 Eclipse 工作台的插件工程就被创建出来了。

插件工程创建后,缺省进入 Plug-in 开发透视图,Plug-in Manifest 编辑器自动打开,显示这个插件工程的基本信息,如对其他插件的依赖,扩展点,构建 (build) 的配置信息等等。由于该工程需要用到其他插件的功能,必须为其添加到其他插件的依赖。在 Plug-in Manifest 编辑器点击 Dependencies 页面,在该页面中的 Required Plug-ins 列表中通过 Add 按钮添加如下的插件:

清单 2
 org.eclipse.jface.text 
 org.eclipse.ltk.core.refactoring 
 org.eclipse.ltk.ui.refactoring 
 org.eclipse.jdt 
 org.eclipse.jdt.core

或者也可以通过直接修改 MANIFEST.MF 文件完成。操作完成后察看 MANIFEST.MF 文件,注意 Require-Bundle 列表中是否出现了新添加的这几项。MANIFEST.MF 文件如下:

清单 3
 Manifest-Version: 1.0 
 Bundle-ManifestVersion: 2 
 Bundle-Name: Annotation Plug-in 
 Bundle-SymbolicName: manage.annotation; singleton:=true 
 Bundle-Version: 1.0.0 
 Bundle-Activator: manage.annotation.AnnotationPlugin 
 Bundle-Localization: plugin 
 Require-Bundle: org.eclipse.ui, 
 org.eclipse.core.runtime, 
 org.eclipse.jface.text, 
 org.eclipse.ltk.core.refactoring, 
 org.eclipse.ltk.ui.refactoring, 
 org.eclipse.jdt, 
 org.eclipse.jdt.core 
 Eclipse-AutoStart: true

在 Plug-in Manifest 编辑器中打开插件清单文件 plugin.xml,可以看到,这个插件扩展了 org.eclipse.ui.actionSets 扩展点,这是一个基本的 Eclipse 工作台的扩展点,通过扩展它,插件可以很简单得对 Eclipse 的菜单、工具条进行扩展。这个 plugin.xml 是"Hello,World"插件模板的清单文件,我们把它改成适合这个工程的文件。清单如下:

清单 4
 <?xml version="1.0" encoding="UTF-8"?> 
 <?eclipse version="3.0"?> 
 <plugin> 
   <extension 
         point="org.eclipse.ui.actionSets"> 
      <actionSet 
            label="Annotation Action Set"
            visible="true"
            id="manage.annotation.actionSet"> 
                     <menu 
               label="%Refactoring.menu.label"
               path="source"
               id="org.eclipse.jdt.ui.refactoring.menu"> 
            <separator name="reorgGroup"/> 
         </menu> 
         <action 
               class="manage.annotation.actions.AnnotationManageAction"
               icon="icons/sample.gif"
               id="manage.annotation.actions.AnnotationManageAction"
               label="%Annotation.manage"
               menubarPath="org.eclipse.jdt.ui.refactoring.menu/reorgGroup"
               toolbarPath="reorgGroup"
               tooltip="Manage Annotation in Java Project"/> 
      </actionSet> 
   </extension> 
 </plugin>

该清单文件表明,在 Refactor 菜单中添加了一个新菜单项"Annotation Manage",并在工具条上相应添加了一个按钮。点击菜单项或者按钮的事件由类"manage.annotation.actions.AnnotationManageAction"处理。

最后需要修改的就是 manage.annotation.actions.AnnotationManageAction 类。它继承了 org.eclipse.ui.IWorkbenchWindowActionDelegate 接口,该接口用于处理各种通过扩展点添加的操作。当菜单项或者按钮被点击时,这个类就被 Eclipse 工作台装载进来,处理转发过来的请求以及接下来的操作。

AnnotationManageAction 被创建后,一旦用户的选择部分有所改变,接口的 selectionChanged 函数就会被触发,告知用户所选择的部分,可以在这个函数中根据用户的选择相应的修改操作的可用性或者其他显示属性。例如在本文的工程中,我们希望只有当用户选择了一个 Java 模型元素时才能使用这个操作,那么就需要在 selectionChanged 中添加如下的代码:

清单 5
		 public void selectionChanged(IAction action, ISelection selection) { 
		 if (selection.isEmpty()) 
			 select = null; 
		 else if (selection instanceof IStructuredSelection) { 
			 IStructuredSelection strut = ((IStructuredSelection) selection); 
			 if (strut.size() != 1) 
				 select = null; 
			 if (strut.getFirstElement() instanceof IJavaElement) 
				 select = (IJavaElement) strut.getFirstElement(); 
		 } else 
			 select = null; 
		
		 action.setEnabled(select != null); 
	 }

selectionChanged 函数的参数 selection 记录了用户选择的部分,我们首先判断它的选择部分的数目是否为一,然后判断这个唯一的选择部分是否为 Java 模型元素,这两个条件任何一个不满足都会导致 action.setEnabled(false) 的执行,这时会弹出如下的对话框说明操作不可用,同时菜单项和按钮都会显示成灰色,直到用户选择了合适的部分时,菜单项和按钮才会实显,就可以进行具体的操作了。

图 4 表明 Action 目前不能执行的对话框
图 4 表明 Action 目前不能执行的对话框

操作的执行是在 AnnotationManageAction 的 run 函数中实现的,例如在本文的工程中,就是弹出 RefactoringWizard 对话框,指导用户完成重构的工作,这些我们将在下面的章节中讲述。


5 扩展 Refactoring 类

通过前面系统构架的介绍,大家知道了 Refactoring 和 RefactoringWizard 是完成 EClipse 重构功能的基础类。在创建好插件工程后,我们就通过扩展 Refactoring 来实现具体的功能。

Refactoring 是所有支持代码转化的类的抽象父类,它在整个流程中与 RefactoringWizard 交互以完成重构的功能,起着非常重要的作用。这些类需要提供以下两类方法:

  • 用于条件检查的方法,判断重构操作大体而言能否执行,以及具体的转化能否完成;
  • 创建 Change 对象的方法,Change 对象描述了所有将要执行的对当前代码的修改操作。

Refactoring 类的典型流程如下所示:

1. 具体的 Refactoring 类被创建。

2. 获得用户选择的要进行重构的对象,初始化该 Refactoring 类。这个由具体的实现类给出相应的方法。

3. 在重构操作开始执行时,首先调用 Refactoring 的 checkInitialConditions(IProgressMonitor) 基于用户选择的对象做一个的初始检查,这个通常由界面自动执行。返回 RefactoringStatus.FATAL 表明初始检查没有通过,重构操作不能继续。

4. 获得进行重构的其他参数,比如,对重命名操作来说就是指新名字。这个通常是由界面根据用户的输入提供的。由具体的实现类给出相应的方法。

5. 获得用户输入参数后,调用 Refactoring 的 checkFinalConditions(IProgressMonitor) 进行剩下的检查,这个通常由界面自动执行,返回 RefactoringStatus.FATAL 表明最后的检查没有通过,重构操作不能继续。

6. 调用 Refactoring 的 createChange(IProgressMonitor) 获得 Change 对象,这个通常由界面自动执行,界面可以根据 Change 对象显示预览界面。

基于以上的介绍,为了实现本文工程中的重构操作,我们需要扩展 Refactoring 类,为它增加一个构造函数,并且具体实现 checkInitialConditions、checkFinalConditions 和 createChange 三个函数。

首先通过菜单 File -> New->Class 弹出创建类的对话框,输入包名 manage.annotation.refactor,类名 AnnotationRefactoring,输入父类 org.eclipse.ltk.core.refactoring.Refactoring,选中"继承抽象方法"复选框,点击完成按钮,一个扩展了 Refactoring 的最基本的类 AnnotationRefactoring 就被创建出来了。

首先为其增加构造函数,以用户选择的 Java 模型元素作为参数。Refactoring 分析这个参数以得到所有相关的可写 Java 文件,作为重构操作的对象,如果该模型元素包含在 Java 文件中,则找到包含它的文件节点;如果该模型元素包含 Java 文件,则找到它的所有子 Java 文件。构造函数代码如下:

清单 6
		 public AnnotationRefactoring(IJavaElement element) { 
		 while (element.getElementType() > IJavaElement.COMPILATION_UNIT) { 
			 element = element.getParent(); 
			 if (element == null) 
				 return; 
		 } 
		 if (element.getElementType() == IJavaElement.COMPILATION_UNIT) { 
			 if (!element.isReadOnly()) 
				 compilationUnits.add(element); 
		 } 
		 if (element.getElementType() < IJavaElement.COMPILATION_UNIT) 
			 findWritableCompilationUnits(element); 
	 }

接着完成 checkInitialConditions 函数,实现初始检查的具体操作。作为示例,在本文工程中我们不进行任何具体的检查操作,只简单得给出初始检查成功的信息,返回 RefactoringStatus.

INFO 以使重构操作继续执行。checkInitialConditions 函数代码如下:

清单 7
		 public RefactoringStatus checkInitialConditions(IProgressMonitor pm) 
			 throws CoreException, OperationCanceledException { 
		 return RefactoringStatus.createInfoStatus("Initial Condition is OK!"); 
	 }

接着完成 checkFinalConditions 函数,实现获得用户输入参数后的后续检查操作。在本文工程中,我们首先收集所有需要添加注释的以 test 开头的方法,判断是否不存在这样的方法,如果不存在给出出错信息,返回 RefactoringStatus.FATAL 以结束重构操作;如果存在这样的方法,则给出后续检查成功的信息,返回 RefactoringStatus.

INFO。checkFinalConditions 函数代码如下:

清单 8
public RefactoringStatus checkFinalConditions(IProgressMonitor pm) 
    throws CoreException, OperationCanceledException { 
    collectChanges(); 
    if (fChangeManager.size() == 0) 
        return RefactoringStatus 
         .createFatalErrorStatus("No testing methods found!"); 
    else return RefactoringStatus.createInfoStatus("Final condition is OK!"); 
}

最后,创建 Change 对象的 createChange 函数是整个重构操作中最核心的代码,它的实现将在下面章节中介绍。


6 使用 AST 构造 Change 对象

当我们找到了修改的位置时,我们有两个选择:

1. 通过 IScanner 接口扫描代码,然后通过 IBuffer 接口直接修改代码

2. 通过遍历和编辑 AST 树进行结构化的修改

DeveloperWorks 提供的文章《扩展 Eclipse 的 Java 开发工具》中,给出了使用 IBuffer 接口的例子。现在我们要讲述使用 AST 来遍历和修改 Java 源代码的方法。

AST 是 abstract syntax tree 的缩写。它是 Eclipse 中的 Java 开发环境 (JDT) 为我们提供的极为强大的源代码解析和编辑工具。

在使用 AST 树提供的功能之前,我们首先要创建 AST 树。由于 AST 树的构建是一项费时的操作,JDT 缺省情况下不将源代码解析为 AST 树。下面的代码演示了获得一个 ICompilationUnit 对应的 AST 树的过程。在 JDT 提供的 API 中,ICompilationUnit 接口用于表示一个可以被编译的源代码文件。在我们提供的例子程序中,我们通过下面的代码将整个文件解析成为了一颗 AST 树。

清单 9
				 ASTParser parser = ASTParser.newParser(AST.JLS3); 
		 parser.setSource(cu); 
		 ASTNode root = parser.createAST(null);

AST 树中的每个节点都是 ASTNode 类型,通过 Visit 模式,我们可以访问一个 ASTNode 包含的所有节点。下面的代码演示了访问一个 AST 节点并获得其中所有的 MethodDeclaration 节点的方法。

清单 10
		 private void getMethods(ASTNode cuu, final List methods) { 
		 cuu.accept(new ASTVisitor() { 
			 public boolean visit(MethodDeclaration node) { 
				 methods.add(node); 
				 return false; 
			 } 
		 }); 
	 }

在收集到了所有的 MethodDeclaration 节点之后,我们就可以通过向 AST 树中插入和删除节点或者修改已有的节点的方法来修改 AST 树了。下面的代码演示了使用 AST 工具为方法添加 @Test Annotation 的功能。

清单 11
		 private boolean collectChanges(CompilationUnit root, 
			 MethodDeclaration method) { 
		 if (method.getName().getFullyQualifiedName().startsWith("test")) { 
			 AST ast = method.getAST(); 
			 if (needTimeout) { 
				 NormalAnnotation na = ast.newNormalAnnotation(); 
				 na.setTypeName(ast.newSimpleName("Test")); 
				 MemberValuePair pair = ast.newMemberValuePair(); 
				 pair.setName(ast.newSimpleName("timeout")); 
				 pair.setValue(ast.newNumberLiteral("500")); 
				 na.values().add(pair); 
				 method.modifiers().add(0, na); 
			 } else { 
				 MarkerAnnotation na = ast.newMarkerAnnotation(); 
				 na.setTypeName(ast.newSimpleName("Test")); 
				 method.modifiers().add(0, na); 
			 } 
			 return true; 
		 } 
		 return false; 
	 }

在 Refactoring 框架中,我们要求对 AST 树的修改并不立刻反映到源代码中。相反,我们需要一个能记录整个修改过程的 Change 对象。Refactoring 框架将利用这个 Change 对象来显示 Priveiw 窗口、进行 Undo 和 Redo 等操作。大致上,我们记录对一个 AST 树的修改从而生成 Change 对象的过程如以下代码所示。

清单 12
        root.recordModifications(); 
    
    // 在这里修改 AST 树…
	 TextEdit edits = root.rewrite(document, cu.getJavaProject() 
			 .getOptions(true)); 
	 TextFileChange change = new TextFileChange("", (IFile) cu 
			 .getResource()); 
	 change.setEdit(edits);

最后,由于 Refactoring 类的 createChange 方法仅返回一个 Change 对象,如果我们需要对多个源代码文件进行修改,我们可以利用 CompositeChange 类将多个 Change 对象封装成一个 Change 对象。这个过程可能类似如下代码所执行的流程

清单 13
		 public Change createChange(IProgressMonitor pm) throws CoreException, 
			 OperationCanceledException { 
		 Change[] changes = new Change[fChangeManager.size()]; 
		 System.arraycopy(fChangeManager.toArray(), 0, changes, 0, 
				 fChangeManager.size()); 
		 CompositeChange change = new CompositeChange( 
				"Add @Override Annotation", changes); 
		 return change; 
	 }

7 扩展 RefactoringWizard 框架

Eclipse 中的 RefactoringWizard 框架扩展了 Eclipse 的 Wizard 框架,关于 Wizard 框架的介绍可以在 Eclipse 的帮助系统中找到,这里我们仅从 OO 设计和架构的角度探讨一下 RefactoringWizard 框架。

我们从 Wizard 相关的几个类开始:

1. WizardPage 类

WizardPage 是一个包含了多个界面元素(比如文本框 Text,按钮 Button)的一个界面组合部分。各个 Page 之间是独立的,是可以动态加载的。WizardPage 类的职责有:

  • 组合 SWT 界面元素,构造出一个界面页。
  • 定义本身界面元素的操作行为。

在 RefactoringWizard 框架中预设了两个通用的属性页:PreviewWizardPage 和 ErrorWizardPage。PreviewWizardPage 类是用来预览重构后的修改,对比代码或其他资源的变化。ErrorWizardPage 类是用来处理条件检查及错误状态通知的。我们只需扩展 RefactoringWizard 框架就可以自动获取这两项强大的功能。

2. Wizard 类

一个 Wizard 就是一个装载一系列 WizardPage 页的容器,Wizard 类的职责有:

  • 装载一系列 WizardPage,构造出一个复杂的界面。
  • 装载领域类来处理具体业务逻辑。( 在 RefactoringWizard 框架中这个类就是 Refactoring 类 )
  • 维护 WizardPage 页之间以及页与领域类之间的数据传递和状态共享。(在这里要补充一点,其实在具体 RefactoringWizard 框架的实现中有专门的类来分担这部分职责。)

我们的界面行为可以千变万化 ( 通过组合不同的 WizardPage),而负责处理业务逻辑的领域类也可以独立的变化,你可以随意扩展 Wizard 的界面功能 (-对扩展开放 ),而不用修改现有 RefactoringWizard 框架 (-对修改封闭 ),这正是 OO 设计的最基本原则-OCP(Open-Close Principle)。

3. WizardDialog 类

这个对话框类的主要职责是构造一个完整的 GUI 界面以及操作界面。它预设了一些按钮(Back,Next,Finish,Cancel)等界面元素,它负责装载 Wizard 类,操作时通过按钮 Back、Next 来在多个 WizardPage 之间切换。

下面我们给出 RefactoringWizard 框架的架构图:

图 5 Refactoring Wizard 架构图
图 5 Refactoring Wizard 架构图

从图 5 中我们可以看到,如果我们把每一个 WizardPage 页看作一项业务,那么 Refactoring 正是处理业务逻辑的控制中心,它封装了所有对业务逻辑的处理,当然它可以在将处理任务委任出去。但请注意,它并不负责实现业务流程,也就是说各业务 ( 各个 Page 界面 ) 之间的逻辑顺序关系不由它维护。

RefactoringWizard 框架充分考虑到了应用的可扩展性,它在 SWT 的 MVC( 模型-视图-控制 ) 元架构模式的基础上,添加了一些新的架构元素。MVC 模式促使业务逻辑与界面分离,界面与控制行为分离,而 RefactoringWizard 框架增强了界面本身分离的特性,它将一个完整的界面分拆成多个页面,用户可以动态组合这些页面或添加新的页面来扩展界面行为。这种特性-界面的动态组合,低耦合,高内聚,封装良好的接口-让我们领略到了 OO 设计的精髓。

下面我们通过以下几个步骤来扩展 RefactoringWizard 框架:

  • 扩展 RefactoringWizardPage
  • 扩展 RefactoringWizard
  • 启动 RefactoringWizard

第一步,扩展 RefactoringWizardPage:首先我们新建一个类 AnnotationRefactoringWizardPage,它需要继承 UserInputWizardPage 类 ( 其父类是 RefactoringWizardPage,而 RefactoringWizardPage 最终实现了 IDialogPage 接口 )。接下来就是实现 IDialogPage 接口的 createControl( … ) 方法,在这个方法里实现你的界面行为,比如我们例子中的 TimeOut 文本框,代码清单如下:

清单 14
		 /** 
	 * create composite to add UI elements 
	 */ 
	 public void createControl(Composite parent) { 
		 // define UI 
		 Composite composite = new Composite(parent, SWT.NONE); 
		 GridLayout lay = new GridLayout(); 
		 lay.numColumns = 2; 
		 composite.setLayout(lay); 
		 btnCheck = new Button(composite, SWT.CHECK); 
		 btnCheck.setText("Add timeout parameter"); 
		 GridData gdBtnCheck = new GridData(); 
		 gdBtnCheck.horizontalSpan = 2; 
		 gdBtnCheck.horizontalAlignment = GridData.FILL; 
		 btnCheck.setLayoutData(gdBtnCheck); 
		 labName = new Label(composite, SWT.WRAP); 
		 labName.setText("TimeOut:"); 
		 GridData gdLabName = new GridData(); 
		 gdLabName.horizontalAlignment = GridData.BEGINNING; 
		 gdLabName.grabExcessHorizontalSpace = true; 
		 labName.setLayoutData(gdLabName); 
		 txtTimeOut = new Text(composite, SWT.SINGLE | SWT.BORDER); 
		 GridData gdTxtTimeOut = new GridData(); 
		 gdTxtTimeOut.horizontalAlignment = GridData.END; 
		 gdLabName.grabExcessHorizontalSpace = true; 
		 txtTimeOut.setLayoutData(gdTxtTimeOut); 
		 txtTimeOut.setText("500"); 
		 // init status 
		 labName.setEnabled(false); 
		 txtTimeOut.setEnabled(false); 
		 // add listener 
		 defineListener(); 
		 // 将 composite 纳入框架的控制
		 setControl(composite); 
		 Dialog.applyDialogFont(composite); 
	 }

在这里我们要特别注意的一点是在定义完我们的界面元素后,需要将自定义的 Composite 纳入框架的控制,就是这行代码:"setControl(composite);"

在我们处理完输入数据检查后进入下一页面之前,我们需要设置页面完成的状态,以及传递输入数据到领域类 Refactoring。我们用以下代码设置好页面完成的状态后,下个页面 ErrorWizardPage 就会处理显示逻辑:

清单 15
		 private void notifyStatus(boolean valid, String message) { 
		 // 设置错误信息
		 setErrorMessage(message); 
		 // 设置页面完成状态
		 setPageComplete(valid); 
	 }

传递输入数据通过以下代码来处理:

清单 16
private void setRefactoring(boolean selection, String text) { 
    AnnotationRefactoring refactoring = (AnnotationRefactoring) getRefactoring();
    refactoring.setNeedTimeout(true); 
    if(selection) { 
        refactoring.setTimeout(Integer.valueOf(txtTimeOut.getText()).intValue());
    } 
}

其中 getRefactoring() 方法是继承自 RefactoringWizardPage 的方法,由于我们的 RefactoringWizard 类装载了 RefactoringWizardPage 和 Refactoring 类,这个方法是从 RefactoringWizard 类中获得的,这里面用到了 Observer 设计模式。 至此,我们完成 RefactoringWizardPage 的扩展。

第二步,扩展 RefactoringWizard:首先我们新建一个类 AnnotationRefactoringWizard,它需要继承 RefactoringWizard 类,这个类中我们只需要加载定义好的 AnnotationRefactoringWizardPage 类和 AnnotationRefactoring 类,当然复杂的处理已经有 RefactoringWizard 框架处理好了。下面我们在构造函数中加载 Refactoring 类:

清单 17
		 public AnnotationRefactoringWizard(Refactoring refactoring) { 
		 super(refactoring, WIZARD_BASED_USER_INTERFACE); 
	 }

然后我们加载我们的 AnnotationRefactoringWizardPage 类,只需重载父类 RefactoringWizard 的 addUserInputPages() 方法就可以:

清单 18
		 protected void addUserInputPages() { 
		 page = new AnnotationRefactoringWizardPage("refactor annotation"); 
		 addPage(page); 
	 }

第三步,启动 RefactoringWizard。扩展好 RefactoringWizard 之后,就需要在用户点击菜单项或是按钮时弹出这个对话框。RefactoringWizard 最好使用 RefactoringWizardOpenOperation 类来打开 ( 当然也可以用 RefactoringWizardDialog)。RefactoringWizardOpenOperation 首先进行重构的初始检查,通过后才打开 RefactoringWinzard 对话框,否则就会打开错误对话框。前面完成创建插件工程时我们提到,弹出 RefactoringWizard 对话框的代码应该放到响应菜单操作的类的 run 函数中。具体到本文工程中,就是把下面的代码放到 AnnotationManageAction 的 run 函数中。这些代码首先依次构造 Refactoring 和 RefacoringWizard 子类 AnnotationRefactoring 和 AnnotationRefactoringWizard,并将 AnnotationRefactoring 的引用传递给 AnnotationRefactoringWizard,然后用 RefactoringWizardOpenOperation 打开 AnnotationRefactoringWizard,弹出向导对话框。

清单 19
 public void run(IAction action) { 
	 Shell shell = window.getShell(); 
	 AnnotationRefactoring refactor = new AnnotationRefactoring(select); 
	 AnnotationRefactoringWizard wizard = new AnnotationRefactoringWizard( 
			 refactor); 
	 RefactoringWizardOpenOperation op = new RefactoringWizardOpenOperation( 
			 wizard); 
	 try { 
		 op.run(shell, "Inserting @Override Annotation"); 
	 } catch (InterruptedException e) { 
		 e.printStackTrace(); 
	 } 
 }

8 小结

在 Eclipse 中有效的利用重构能够大大的减轻软件开发人员的工作负担,提高软件的健壮性。然而,目前重构仍然处在一个工具缺乏的时代。以 Eclipse 为例,只有 JDT 提供的重构工具最为完善,而针对其他语言例如 C++、Python 等的开发环境,都缺乏对应的重构功能。

通过本文提供的方法,我们能够有效的利用 Eclipse 中的重构框架创建新的重构,从而进一步提高已有开发环境的效率。


9 参考资料

1. 在线教程:重构 http://www.ibm.com/developerworks/cn/views/java/tutorials.jsp?cv_doc_id=85203

2. 《任何人都可以重构》是 developerWorks 上 David Gallardo 撰写的优秀文章

3. 尽管 JUnit 4 还没有正式发布,但我们已经有一片优秀的文章《 JUnit 4 抢先看》来帮助我们了解 JUnit 4 的新特性, http://www.ibm.com/developerworks/cn/java/j-junit4.html

4. 通过 开发 Eclipse 插件了解基本的插件开发知识。

5. 文章《扩展 Eclipse 的 Java 开发工具》介绍了在 Eclipse 中修改 Java 源代码的另一种方法。http://www.ibm.com/developerworks/cn/linux/opensource/os-ecjdt/

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


这是您第一次登陆到 developerWorks,已经自动为您创建了您的概要文件。 选择您概要文件中可以公开的信息的信息(如姓名、国家/地区,以及公司),这些信息同时也会与您所发布的内容相关联。 您可以随时更新您的 IBM 账号。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology
ArticleID=99224
ArticleTitle=在 Eclipse 中创建新的重构功能
publish-date=11012005