get() 和 set()。 清单 1. Bag 类 |
当匿名类变得很大,其中的代码难以阅读的时候,您就应该考虑将这个匿名类变成严格意义上的类;为了保持封装性(换句话说,就是将它隐藏起来,使得不必知道它的外部类不知道它),您应该将其变成嵌套类,而不是顶级类。您可以在这个匿名类的内部点击,然后选择 Refactor > Convert Anonymous Class to Nested 就可以了。当出现确认对话框的时候,为这个类输入名称,比如 BagImpl,然后选择 Preview 或者 OK。这样,代码就变成了如清单2所示的情形。
|
当您想让其他的类使用某个嵌套类时,Convert Nested Type to Top Level 就很有用了。比方说,您可以在一个类中使用值对象,就像上面的 BagImpl 类那样。如果您后来又决定应该在多个类之间共享这个数据,那么重构操作就能从这个嵌套类中创建新的类文件。您可以在源代码文件中高亮选中类名称(或者在 Outline 视图中点击类的名称),然后选择 Refactor > Convert Nested Type to Top Level,这样就实现了重构。
这种重构要求您为装入实例提供一个名字。重构工具也会提供建议的名称,比如 example,您可以接受这个名字。这个名字的意思过一会儿就清楚了。点击 OK 之后,外层类 BagExample 就会变成清单3所示的样子。
|
请注意,当一个类是嵌套类的时候,它可以访问其外层类的成员。为了保留这种功能,重构过程将一个装入类 BagExample 的实例放在前面那个嵌套类中。这就是之前要求您输入名称的实例变量。同时也创建了用于设置这个实例变量的构造函数。重构过程创建的新类 BagImpl 如清单4所示。
清单 4. BagImpl 类 |
如果您的情况与这个例子相同,不需要保留对 BagExample 的访问,您也可以很安全地删除这个实例变量与构造函数,将 BagExample 类中的代码改成缺省的无参数构造函数。
在类继承关系内移动成员
还有两个重构工具,Push Down 和 Pull Up,分别实现将类方法或者属性从一个类移动到其子类或父类中。假设您有一个名为 Vehicle 的抽象类,其定义如清单5所示。
|
您还有一个 Vehicle 的子类,类名为 Automobile,如清单6所示。
|
请注意,Vehicle 有一个属性是 motor。如果您知道您将永远只处理汽车,那么这样做就好了;但是如果您也允许出现划艇之类的东西,那么您就需要将 motor 属性从 Vehicle 类下放到 Automobile 类中。为此,您可以在 Outline 视图中选择 motor,然后选择 Refactor > Push Down。
Eclipse 还是挺聪明的,它知道您不可能总是单单移动某个属性本身,因此还提供了 Add Required 按钮,不过在 Eclipse 2.1 中,这个功能并不总是能正确地工作。您需要验证一下,看所有依赖于这个属性的方法是否都推到了下一层。在本例中,这样的方法有两个,即与 motor 相伴的 getter 和 setter 方法,如图3所示。
图 3. 加入所需的成员
在按过 OK 按钮之后,motor 属性以及 getMotor() 和 setMotor() 方法就会移动到 Automobile 类中。清单7显示了在进行了这次重构之后 Automobile 类的情形。
|
Pull Up 重构与 Push Down 几乎相同,当然 Pull Up 是将类成员从一个类中移到其父类中,而不是子类中。如果您稍后改变主意,决定还是把 motor 移回到 Vehicle 类中,那么您也许就会用到这种重构。同样需要提醒您,一定要确认您是否选择了所有必需的成员。
Automobile 类中具有成员 motor,这意味着您如果创建另一个子类,比方说 Bus,您就还需要将 motor(及其相关方法)加入到 Bus 类中。有一种方法可以表示这种关系,即创建一个名为 Motorized 的接口,Automobile 和 Bus 都实现这个接口,但是 RowBoat 不实现。
创建 Motorized 接口最简单的方法是在 Automobile 上使用 Extract Interface 重构。为此,您可以在 Outline 视图中选择 Automobile,然后从菜单中选择 Refactor > Extract Interface。您可以在弹出的对话框中选择您希望在接口中包含哪些方法,如图4所示。
图 4. 提取 Motorized 接口
点击 OK 之后,接口就创建好了,如清单8所示。
清单 8. Motorized 接口 |
同时,Automobile 的类声明也变成了下面的样子:
|
使用父类
本重构工具类型中最后一个是 User Supertyp Where Possible。想象一个用来管理汽车细帐的应用程序。它自始至终都使用 Automobile 类型的对象。如果您想处理所有类型的交通工具,那么您就可以用这种重构将所有对 Automobile 的引用都变成对 Vehicle 的引用(参看图5)。如果您在代码中用 instanceof 操作执行了任何类型检查的话,您将需要决定在这些地方适用的是原先的类还是父类,然后选中第一个选项“Use the selected supertype in 'instanceof' expressions”。
图 5. 将 Automobile 改成其父类 Vehicle
使用父类的需求在 Java 语言中经常出现,特别是在使用了 Factory Method 模式的情况下。这种模式的典型实现方式是创建一个抽象类,其中具有静态方法 create(),这个方法返回的是实现了这个抽象类的一个具体对象。如果需创建的具体对象的类型依赖于实现的细节,而调用类对实现细节并不感兴趣的情况下,可以使用这一模式。
改变类内部的代码
最大一类重构是实现了类内部代码重组的重构方法。在所有的重构方法中,只有这类方法允许您引入或者移除中间变量,根据原有方法中的部分代码创建新方法,以及为属性创建 getter 和 setter 方法。
提取与内嵌
有一些重构方法是以 Extract 这个词开头的:Extract Method、Extract Local Variable 以及Extract Constants。第一个 Extract Method 的意思您可能已经猜到了,它根据您选中的代码创建新的方法。我们以清单8中那个类的 main() 方法为例。它首先取得命令行选项的值,如果有以 -D 开头的选项,就将其以名-值对的形式存储在一个 Properties 对象中。
|
将一部分代码从一个方法中取出并放进另一个方法中的原因主要有两种。第一种原因是这个方法太长,并且完成了两个以上逻辑上截然不同的操作。(我们不知道上面那个 main() 方法还要处理哪些东西,但是从现在掌握的证据来看,这不是从其中提取出一个方法的理由。)另一种原因是有一段逻辑上清晰的代码,这段代码可以被其他方法重用。比方说在某些时候,您发现自己在很多不同的方法中都重复编写了相同的几行代码。那就有可能是需要重构的原因了,不过除非真的需要重用这部分代码,否则您很可能并不会执行重构。
假设您还需要在另外一个地方解析名-值对,并将其放在 Properties 对象中,那么您可以将包含 StringTokenizer 声明和下面的 if 语句的这段代码抽取出来。为此,您可以高亮选中这段代码,然后从菜单中选择 Refactor > Extract Method。您需要输入方法名称,这里输入 addProperty,然后验证这个方法的两个参数,Properties prop 和 Strings。清单9显示由 Eclipse 提取了 addProp() 方法之后类的情况。
|
Extract Local Variable 重构取出一段被直接使用的表达式,然后将这个表达式首先赋值给一个局部变量。然后在原先使用那个表达式的地方使用这个变量。比方说,在上面的方法中,您可以高亮选中对 st.nextToken() 的第一次调用,然后选择 Refactor > Extract Local Variable。您将被提示输入一个变量名称,这里输入 key。请注意,这里有一个将被选中表达式所有出现的地方都替换成新变量的引用的选项。这个选项通常是适用的,但是对这里的 nextToken() 方法不适用,因为这个方法(显然)在每一次调用的时候都返回不同的值。确认这个选项未被选中。参见图6。
图 6. 不全部替换所选的表达式
接下来,在第二次调用 st.nextToken() 的地方重复进行重构,这一次调用的是一个新的局部变量 value。清单10显示了这两次重构之后代码的情形。
|
用这种方式引入变量有几点好处。首先,通过为表达式提供有意义的名称,可以使得代码执行的任务更加清晰。第二,代码调试变得更容易,因为我们可以很容易地检查表达式返回的值。最后,在可以用一个变量替换同一表达式的多个实例的情况下,效率将大大提高。
Extract Constant 与 Extract Local Variable 相似,但是您必须选择静态常量表达式,重构工具将会把它转换成静态的 final 常量。这在将硬编码的数字和字符串从代码中去除的时候非常有用。比方说,在上面的代码中我们用“-D”这一命令行选项来定义名-值对。先将“-D”高亮选中,选择 Refactor > Extract Constant,然后输入 DEFINE 作为常量的名称。重构之后的代码如清单11所示:
清单 11. 重构之后的代码 |
对于每一种 Extract... 类的重构,都存在对应的 Inline... 重构,执行与之相反的操作。比方说,如果您高亮选中上面代码中的变量 s,选择 Refactor > Inline...,然后点击 OK,Eclipse 就会在调用 addProp() 的时候直接使用 args[i].substring(2) 这个表达式,如下所示:
|
这样比使用临时变量效率更高,代码也变得更加简要,至于这样的代码是易读还是含混,就取决于您的观点了。不过一般说来,这样的内嵌重构没什么值得推荐的地方。
您可以按照用内嵌表达式替换变量的相同方法,高亮选中方法名,或者静态 final 常量,然后从菜单中选择 Refactor > Inline...,Eclipse 就会用方法的代码替换方法调用,或者用常量的值替换对常量的引用。
封装属性
通常我们认为将对象的内部结构暴露出来是一种不好的做法。这也正是 Vehicle 类及其子类都具有 private 或者 protected 属性,而用 public setter 和 getter 方法来访问属性的原因。这些方法可以用两种不同的方式自动生成。
第一种生成这些方法的方式是使用 Source > Generate Getter and Setter 菜单。这将会显示一个对话框,其中包含所有尚未存在的 getter 和 setter 方法。不过因为这种方式没有用新方法更新对这些属性的引用,所以并不算是重构;必要的时候,您必须自己完成更新引用的工作。这种方式可以节约很多时间,但是最好是在一开始创建类的时候,或者是向类中加入新属性的时候使用,因为这些时候还不存在对属性的引用,所以不需要再修改其他代码。
第二种生成 getter 和 setter 方法的方式是选中某个属性,然后从菜单中选择 Refactor > Encapsulate Field。这种方式一次只能为一个属性生成 getter 和 setter 方法,不过它与 Source > Generate Getter and Setter 相反,可以将对这个属性的引用改变成对新方法的调用。
例如,我们可以先创建一个新的简版 Automobile 类,如清单12所示。
|
接下来,创建一个类实例化了 Automobile 的类,并直接访问 make 属性,如清单13所示。
|
现在封装 make 属性。先高亮选中属性名称,然后选择 Refactor > Encapsulate Field。在弹出的对话框中输入 getter 和 setter 方法的名称——如您所料,缺省的方法名称分别是 getMake() 和 setMake()。您也可以选择与这个属性处在同一个类中的方法是继续直接访问该属性,还是像其他类那样改用这些访问方法。(有一些人非常倾向于使用这两种方式的某一种,不过碰巧在这种情况下您选择哪一种方式都没有区别,因为 Automobile 中没有对 make 属性的引用。)
图7. 封装属性
点击 OK 之后,Automobile 类中的 make 属性就变成了私有属性,也同时具有了 getMake() 和 setMake() 方法。
|
AutomobileTest 类也要进行更新,以便使用新的访问方法,如清单15所示。
|
改变方法的签名
本文介绍的最后一个重构方法也是最难以使用的方法:Change Method Signature(改变方法的签名)。这种方法的功能显而易见——改变方法的参数、可见性以及返回值的类型。而进行这样的改变对于调用这个方法的其他方法或者代码会产生什么影响,就不是那么显而易见了。这么也没有什么魔方。如果代码的改变在被重构的方法内部引发了问题——变量未定义,或者类型不匹配——重构操作将对这些问题进行标记。您可以选择是接受重构,稍后改正这些问题,还是取消重构。如果这种重构在其他的方法中引发问题,就直接忽略这些问题,您必须在重构之后亲自修改。
为澄清这一点,考虑清单16中列出的类和方法。
清单 16. MethodSigExample 类 |
上面这个类中的 test() 方法被另一个类中的方法调用,如清单17所示。
|
在第一个类中高亮选中 test,然后选择 Refactor > Change Method Signature。您将看到如图8所示的对话框。
图 8. Change Method Signature 选项
第一个选项是改变该方法的可见性。在本例中,将其改变为 protected 或者 private,这样第二个类的 callTest() 方法就不能访问这个方法了。(如果这两个类在不同的包中,将访问方法设为缺省值也会引起这样的问题。) Eclipse 在进行重构的时候不会将这些问题标出,您只有自己选择适当的值。
下面一个选项是改变返回值类型。如果将返回值改为 float,这不会被标记成错误,因为 test() 方法返回语句中的 int 会自动转换成 float。即便如此,在第二个类的 callTest() 方法中也会引起问题,因为 float 不能转换成 int。您需要将 test() 的返回值改为 int,或者是将 callTest() 中的 r 改为 float。
如果将第一个参数的类型从 String 变成 int,那么也得考虑相同的问题。在重构的过程中这些问题将会被标出,因为它们会在被重构的方法内部引起问题:int 不具有方法 length()。然而如果将其变成 StringBuffer,问题就不会标记出来,因为 StringBuffer 的确具有方法 length()。当然这会在 callTest() 方法中引起问题,因为它在调用 test() 的时候还是把一个 String 传递进去了。
前面提到过,在重构引发了问题的情况下,不管问题是否被标出,您都可以一个一个地修正这些问题,以继续下去。还有一种方法,就是先行修改这些错误。如果您打算删除不再需要的参数 i,那么可以先从要进行重构的方法中删除对它的引用。这样删除参数的过程就更加顺利了。
最后一件需要解释的事情是 Default Value 选项。这一选项值仅适用于将参数加入方法签名中的情况。比方说,如果我们加入了一个类型为 String 的参数,参数名为 n,其缺省值为 world,那么在 callTest() 方法中调用 test() 的代码就变成下面的样子:
|
在这场有关 Change Method Signature 重构的看似可怕的讨论中,我们并没有隐藏其中的问题,但却一直没有提到,这种重构其实是非常强大的工具,它可以节约很多时间,通常您必须进行仔细的计划才能成功地使用它。
结束语
Eclipse 提供的工具使重构变得简单,熟悉这些工具将有助于您提高效率。敏捷开发方法采用迭代方式增加程序特性,因此需要依赖于重构技术来改变和扩展程序的设计。但即便您并没有使用要求进行正式重构的方法,Eclipse 的重构工具还是可以在进行一般的代码修改时提供节约时间的方法。如果您花些时间熟悉这些工具,那么当出现可以利用它们的情况时,您就能意识到所花费的时间是值得的。
参考资料
书籍
- 有关重构的核心着作是 Refactoring: Improving the Design of Existing Code, 作者 Martin Fowler、Kent Beck、John Brant、William Opdyke 和 Don Roberts(Addison-Wesley,1999年)。
- 重构是一种正在发展的方法,在 Eclipse In Action: A Guide for Java Developers (Manning, 2003年)一书中,作者 David Gallardo,Ed Burnette 以及 Robert McGovern 从在 Eclipse 中设计和开发项目的角度讨论了这一话题。
- 模式(如本文中提到的 Factory Method 模式)是理解和讨论面向对象设计的重要工具。这方面的经典着作是 Design Patterns: Elements of Reusable Object-Oriented Software,作者为 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides (Addison-Wesley,1995年)。
- Design Patterns 中的例子是用 C++ 写成的,这对于 Java 程序员是不小的障碍;Mark Grand 所着的 Patterns in Java, Volume One: A Catalog of Reusable Design Patterns Illustrated with UML(Wiley,1998年)将模式翻译成了 Java 语言。
- 有关敏捷编程的一个变种,请参看 Kent Beck 所着的 Extreme Programming Explained: Embrace Change(Addison-Wesley,1999年)
Web 站点
- Martin Fowler 的个人网站 是 Web 上的重构技术中心。
- 有关用 JUnit 进行单元测试的更多信息,请访问 JUnit 网站。
- Refactoring in VS.NET Whidbey.PDF






