http://www-900.ibm.com/developerWorks/cn/linux/opensource/os-ecref/index.shtml
Eclipse 提供了一组强大的自动重构(refactoring)功能,这些功能穿插在其他功能当中,使您能够重命名 Java 元素,移动类和包,从具体的类中创建接口,将嵌套的类变成顶级类,以及从旧方法的代码片断中析取出新的方法。您熟悉了 Eclipse 的重构工具之后,就掌握了一种提高生产率的好方法。本文综览 Eclipse 的重构特性,并通过例子阐明了使用这些特性的方法与原因。
为什么重构?
重构是指在不改变程序功能的前提下改变其结构。重构是一项功能强大的技术,但是执行起来需要倍加小心才行。主要的危险在于可能在不经意中引入一些错误,尤其是在进行手工重构的时候更是如此。这种危险引发了对重构技术的普遍批评:当代码不会崩溃的时候为什么要修改它呢?
您需要进行代码重构的原因可能有以下几个:传说中的第一个原因是:需要继承为某个古老产品而开发的年代久远的代码,或者突然碰到这些代码。最初的开发团队已经不在了。我们必须创建增加了新特性的新版本软件,但是这些代码已经无法理解了。新的开发队伍夜以继日地工作,破译代码然后映射代码,经过大量的规划与设计之后,人们将这些代码分割成碎片。历经重重磨难之后,所有这些东西都按照新版本的要求归位了。这是英雄般的重构故事,几乎没有人能在经历了这些之后活着讲述这样的故事。
还有一种现实一些的情况是项目中加入了新的需求,需要对设计进行修改。至于是因为在最初的规划过程中失察,还是由于采用了迭代式的开发过程(比如敏捷开发,或者是测试驱动的开发)而在开发过程中有意引入需求,这两者并没有实质性的区别。这样的重构的规模要小得多,其内容一般涉及通过引入接口或者抽象类来更改类的继承关系,以及对类进行分割和重新组织,等等。
重构的最后一个原因是,当存在可用的自动重构工具时,可以有一个用来预先生成代码的快捷方式——就好比在您无法确定如何拼写某个单词的时候,可以用某种拼写检查工具输入这个单词。比如说,您可以用这种平淡无奇的重构方法生成 getter 和 setter 方法,一旦熟悉了这样的工具,它就可以为您节省很多的时间。
Eclipse 的重构工具无意进行英雄级的重构——适合这种规模的工具几乎没有——但是不论是否用到敏捷开发技术,Eclipse 的工具对于一般程序员修改代码的工作都具有无法衡量的价值。毕竟任何复杂的操作只要能够自动进行,就可以不那么烦闷了。只要您知道 Eclipse 实现了什么样的重构工具,并理解了它们的适用情况,您的生产力就会得到极大的提高。
要降低对代码造成破坏的风险,有两种重要的方法。第一种方法是对代码进行一套完全彻底的单元测试:在重构之前和之后都必须通过这样的测试。第二种方法是使用自动化的工具来进行重构,比如说 Eclipse 的重构特性。
将彻底的测试与自动化重构结合起来就会更加有效了,这样重构也就从一种神秘的艺术变成了有用的日常工具。为了增加新的功能或者改进代码的可维护性,我们可以在不影响原有代码功能的基础上迅速且安全地改变其结构。这种能力会对您设计和开发代码的方式产生极大的影响,即便是您没有将其结合到正式的敏捷方法中也没有关系。
Eclipse 中重构的类型
Eclipse 的重构工具可以分为三大类(下面的顺序也就是这些工具在 Refactoring 菜单中出现的顺序):
对代码进行重命名以及改变代码的物理结构,包括对属性、变量、类以及接口重新命名,还有移动包和类等。
改变类一级的代码逻辑结构,包括将匿名类转变为嵌套类,将嵌套类转变为顶级类、根据具体的类创建接口,以及从一个类中将方法或者属性移到子类或者父类中。
改变一个类内部的代码,包括将局部变量变成类的属性、将某个方法中选中部分的代码变成一个独立的方法、以及为属性生成 getter 和 setter 方法。
还有几个重构工具并不能完全归入这三个种类,特别是 Change Method Signature,不过在本文中还是将这个工具归入第三类。除了这种例外情况以外,本文下面几节都是按照上面的顺序来讨论 Eclipse 重构工具的。
物理重组与重命名
显然,您即便没有特别的工具,也可以在文件系统中重命名文件或者是移动文件,但是如果操作对象是 Java 源代码文件,您就需要编辑很多文件,更新其中的 import 或 package 语句。与此类似,用某种文本编辑器的搜索与替换功能也可以很容易地给类、方法和变量重新命名,但是这样做的时候必须十分小心,因为不同的类可能具有名称相似的方法或者变量;要是从头到尾检查项目中所有的文件,来保证每个东西的标识和修改的正确性,那可真够乏味的。
Eclipse 的 Rename 和 Move 工具能够十分聪明地在整个项目中完成这样的修改,而不需要用户的干涉。这是因为 Eclipse 可以理解代码的语义,从而能够识别出对某个特定方法、变量或者类名称的引用。简化这一任务有助于确保方法、变量和类的名称能够清晰地指示其用途。
我们经常可以发现代码的名字不恰当或者令人容易误解,这是因为代码与最初设计的功能有所不同。比方说,某个用来在文件中查找特定单词的程序也许会扩展为在 Web 页面中通过 URL 获取 InputStream 的操作。如果这一输入流最初叫做 file ,那么就应该修改它的名字,以便能反映其新增的更加一般的特性,比方说 sourceStream。开发人员经常无法成功地修改这些名称,因为这个过程是十分混乱和乏味的。这当然也会把下一个不得不对这些类进行操作的开发人员弄糊涂。
要对某个 Java 元素进行重命名,只需要简单地从 Package Explorer 视图中点击这个元素,或者从Java 源代码文件中选中这个元素,然后选择菜单项 Refactor > Rename。在对话框中输入新的名称,然后选择是否需要 Eclipse 也改变对这个名称的引用。实际显示出来的确切内容与您所选元素的类型有关。比方说,如果选择的属性具有 getter 和 setter 方法,那么也就可以同时更新这些方法的名称,以反映新的属性。图1显示了一个简单的例子。
图 1. 重命名一个局部变量
就像所有的 Eclipse 重构操作一样,当您指定了全部用来执行重构的必要信息之后,您就可以点击 Preview 按钮,然后在一个对话框中对比 Eclipse 打算进行哪些变更,您可以分别否决或者确认每一个受到影响的文件中的每一项变更。如果您对于 Eclipse 正确执行变更的能力有信心的话,您可以只按下 OK 按钮。显然,如果您不确定重构到底做了什么事情,您就会想先预览一下,但是对于 Rename 和 Move 这样简单的重构而言,通常没有必要预览。
Move 操作与 Rename 十分相似:您选择某个 Java 元素(通常是一个类),为其指定一个新位置,并定义是否需要更新引用。然后,您可以选择 Preview 检查变更情况,或者选择 OK 立即执行重构,如图2所示。
图 2. 将类从一个包移到另一个包
在某些平台上(特别是 Windows),您还可以在 Package Explorer 视图中通过简单拖放的方法将类从一个包或者文件夹中移到另一个包或文件夹中。所有的引用都会自动更新。
重新定义类的关系
Eclipse 中有大量的重构工具,使您能够自动改变类的关系。这些重构工具并没有 Eclipse 提供的其他工具那么常用,但是很有价值,因为它们能够执行非常复杂的任务。可以说,当它们用得上的时候,就会非常有用。
提升匿名类与嵌套类
Convert Anonymous Class(转换匿名类)和 Convert Nested Type(转换嵌套类)这两种重构方法比较相似,它们都将某个类从其当前范围移动到包含这个类的范围上。
匿名类是一种语法速写标记,使您能够在需要实现某个抽象类或者接口的地方创建一个类的实例,而不需要显式提供类的名称。比如在创建用户界面中的监听器时,就经常用到匿名类。在清单1中,假设 Bag 是在其他地方定义的一个接口,其中声明了两个方法,get() 和 set()。
清单 1. Bag 类
public class BagExample
{
void processMessage(String msg)
{
Bag bag = new Bag()
{
Object o;
public Object get()
{
return o;
}
public void set(Object o)
{
this.o = o;
}
};
bag.set(msg);
MessagePipe pipe = new MessagePipe();
pipe.send(bag);
}
}
当匿名类变得很大,其中的代码难以阅读的时候,您就应该考虑将这个匿名类变成严格意义上的类;为了保持封装性(换句话说,就是将它隐藏起来,使得不必知道它的外部类不知道它),您应该将其变成嵌套类,而不是顶级类。您可以在这个匿名类的内部点击,然后选择 Refactor > Convert Anonymous Class to Nested 就可以了。当出现确认对话框的时候,为这个类输入名称,比如 BagImpl,然后选择 Preview 或者 OK。这样,代码就变成了如清单2所示的情形。
清单 2. 经过重构的 Bag 类
public class BagExample
{
private final class BagImpl implements Bag
{
Object o;
public Object get()
{
return o;
}
public void set(Object o)
{
this.o = o;
}
}
void processMessage(String msg)
{
Bag bag = new BagImpl();
bag.set(msg);
MessagePipe pipe = new MessagePipe();
pipe.send(bag);
}
}
当您想让其他的类使用某个嵌套类时,Convert Nested Type to Top Level 就很有用了。比方说,您可以在一个类中使用值对象,就像上面的 BagImpl 类那样。如果您后来又决定应该在多个类之间共享这个数据,那么重构操作就能从这个嵌套类中创建新的类文件。您可以在源代码文件中高亮选中类名称(或者在 Outline 视图中点击类的名称),然后选择 Refactor > Convert Nested Type to Top Level,这样就实现了重构。
这种重构要求您为装入实例提供一个名字。重构工具也会提供建议的名称,比如 example,您可以接受这个名字。这个名字的意思过一会儿就清楚了。点击 OK 之后,外层类 BagExample 就会变成清单3所示的样子。
清单 3. 经过重构的 Bag 类
public class BagExample
{
void processMessage(String msg)
{
Bag bag = new BagImpl(this);
bag.set(msg);
MessagePipe pipe = new MessagePipe();
pipe.send(bag);
}
}
请注意,当一个类是嵌套类的时候,它可以访问其外层类的成员。为了保留这种功能,重构过程将一个装入类 BagExample 的实例放在前面那个嵌套类中。这就是之前要求您输入名称的实例变量。同时也创建了用于设置这个实例变量的构造函数。重构过程创建的新类 BagImpl 如清单4所示。
清单 4. BagImpl 类
final class BagImpl implements Bag
{
private final BagExample example;
/**
* @paramBagExample
*/
BagImpl(BagExample example)
{
this.example = example;
// TODO Auto-generated constructor stub
}
Object o;
public Object get()
{
return o;
}
public void set(Object o)
{
this.o = o;
}
}
如果您的情况与这个例子相同,不需要保留对 BagExample 的访问,您也可以很安全地删除这个实例变量与构造函数,将 BagExample 类中的代码改成缺省的无参数构造函数。
在类继承关系内移动成员
还有两个重构工具,Push Down 和 Pull Up,分别实现将类方法或者属性从一个类移动到其子类或父类中。假设您有一个名为 Vehicle 的抽象类,其定义如清单5所示。
清单 5. 抽象的 Vehicle 类
public abstract class Vehicle
{
protected int passengers;
protected String motor;
public int getPassengers()
{
return passengers;
}
public void setPassengers(int i)
{
passengers = i;
}
public String getMotor()
{
return motor;
}
public void setMotor(String string)
{
motor = string;
}
}
您还有一个 Vehicle 的子类,类名为 Automobile,如清单6所示。
清单6. Automobile 类
public class Automobile extends Vehicle
{
private String make;
private String model;
public String getMake()
{
return make;
}
public String getModel()
{
return model;
}
public void setMake(String string)
{
make = string;
}
public void setModel(String string)
{
model = string;
}
}
请注意,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 类的情形。
清单 7. 经过重构的 Automobile 类
public class Automobile extends Vehicle
{
private String make;
private String model;
protected String motor;
public String getMake()
{
return make;
}
public String getModel()
{
return model;
}
public void setMake(String string)
{
make = string;
}
public void setModel(String string)
{
model = string;
}
public String getMotor()
{
return motor;
}
public void setMotor(String string)
{
motor = string;
}
}
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 接口
public interface Motorized
{
public abstract String getMotor();
public abstract void setMotor(String string);
}
同时,Automobile 的类声明也变成了下面的样子:
public class Automobile extends Vehicle implements Motorized
使用父类
本重构工具类型中最后一个是 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 对象中。
清单 8. main()
import java.util.Properties;
import java.util.StringTokenizer;
public class StartApp
{
public static void main(String[] args)
{
Properties props = new Properties();
for (int i= 0; i < args.length; i++)
{
if(args[i].startsWith("-D"))
{
String s = args[i].substring(2);
StringTokenizer st = new StringTokenizer(s, "=");
if(st.countTokens() == 2)
{
props.setProperty(st.nextToken(), st.nextToken());
}
}
}
//continue...
}
}
将一部分代码从一个方法中取出并放进另一个方法中的原因主要有两种。第一种原因是这个方法太长,并且完成了两个以上逻辑上截然不同的操作。(我们不知道上面那个 main() 方法还要处理哪些东西,但是从现在掌握的证据来看,这不是从其中提取出一个方法的理由。)另一种原因是有一段逻辑上清晰的代码,这段代码可以被其他方法重用。比方说在某些时候,您发现自己在很多不同的方法中都重复编写了相同的几行代码。那就有可能是需要重构的原因了,不过除非真的需要重用这部分代码,否则您很可能并不会执行重构。
假设您还需要在另外一个地方解析名-值对,并将其放在 Properties 对象中,那么您可以将包含 StringTokenizer 声明和下面的 if 语句的这段代码抽取出来。为此,您可以高亮选中这段代码,然后从菜单中选择 Refactor > Extract Method。您需要输入方法名称,这里输入 addProperty,然后验证这个方法的两个参数,Properties prop 和 Strings。清单9显示由 Eclipse 提取了 addProp() 方法之后类的情况。
清单 9. 提取出来的 addProp()
import java.util.Properties;
import java.util.StringTokenizer;
public class Extract
{
public static void main(String[] args)
{
Properties props = new Properties();
for (int i = 0; i < args.length; i++)
{
if (args[i].startsWith("-D"))
{
String s = args[i].substring(2);
addProp(props, s);
}
}
}
private static void addProp(Properties props, String s)
{
StringTokenizer st = new StringTokenizer(s, "=");
if (st.countTokens() == 2)
{
props.setProperty(st.nextToken(), st.nextToken());
}
}
}
Extract Local Variable 重构取出一段被直接使用的表达式,然后将这个表达式首先赋值给一个局部变量。然后在原先使用那个表达式的地方使用这个变量。比方说,在上面的方法中,您可以高亮选中对 st.nextToken() 的第一次调用,然后选择 Refactor > Extract Local Variable。您将被提示输入一个变量名称,这里输入 key。请注意,这里有一个将被选中表达式所有出现的地方都替换成新变量的引用的选项。这个选项通常是适用的,但是对这里的 nextToken() 方法不适用,因为这个方法(显然)在每一次调用的时候都返回不同的值。确认这个选项未被选中。参见图6。
图 6. 不全部替换所选的表达式
接下来,在第二次调用 st.nextToken() 的地方重复进行重构,这一次调用的是一个新的局部变量 value。清单10显示了这两次重构之后代码的情形。
清单 10. 重构之后的代码
private static void addProp(Properties props, String s)
{
StringTokenizer st = new StringTokenizer(s, "=");
if(st.countTokens() == 2)
{
String key = st.nextToken();
String value = st.nextToken();
props.setProperty(key, value);
}
}
用这种方式引入变量有几点好处。首先,通过为表达式提供有意义的名称,可以使得代码执行的任务更加清晰。第二,代码调试变得更容易,因为我们可以很容易地检查表达式返回的值。最后,在可以用一个变量替换同一表达式的多个实例的情况下,效率将大大提高。
Extract Constant 与 Extract Local Variable 相似,但是您必须选择静态常量表达式,重构工具将会把它转换成静态的 final 常量。这在将硬编码的数字和字符串从代码中去除的时候非常有用。比方说,在上面的代码中我们用“-D”这一命令行选项来定义名-值对。先将“-D”高亮选中,选择 Refactor > Extract Constant,然后输入 DEFINE 作为常量的名称。重构之后的代码如清单11所示:
清单 11. 重构之后的代码
public class Extract
{
private static final String DEFINE = "-D";
public static void main(String[] args)
{
Properties props = new Properties();
for (int i = 0; i < args.length; i++)
{
if (args[i].startsWith(DEFINE))
{
String s = args[i].substring(2);
addProp(props, s);
}
}
}
// ...
对于每一种 Extract... 类的重构,都存在对应的 Inline... 重构,执行与之相反的操作。比方说,如果您高亮选中上面代码中的变量 s,选择 Refactor > Inline...,然后点击 OK,Eclipse 就会在调用 addProp() 的时候直接使用 args[i].substring(2) 这个表达式,如下所示:
if(args[i].startsWith(DEFINE))
{
addProp(props,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所示。
清单 12. 简单的 Automobile 类
public class Automobile extends Vehicle
{
public String make;
public String model;
}
接下来,创建一个类实例化了 Automobile 的类,并直接访问 make 属性,如清单13所示。
清单 13. 实例化 Automobile
public class AutomobileTest
{
public void race()
{
Automobilecar1 = new Automobile();
car1.make= "Austin Healy";
car1.model= "Sprite";
// ...
}
}
现在封装 make 属性。先高亮选中属性名称,然后选择 Refactor > Encapsulate Field。在弹出的对话框中输入 getter 和 setter 方法的名称——如您所料,缺省的方法名称分别是 getMake() 和 setMake()。您也可以选择与这个属性处在同一个类中的方法是继续直接访问该属性,还是像其他类那样改用这些访问方法。(有一些人非常倾向于使用这两种方式的某一种,不过碰巧在这种情况下您选择哪一种方式都没有区别,因为 Automobile 中没有对 make 属性的引用。)
图7. 封装属性
点击 OK 之后,Automobile 类中的 make 属性就变成了私有属性,也同时具有了 getMake() 和 setMake() 方法。
清单 14. 经过重构的 Automobile 类
public class Automobile extends Vehicle
{
private String make;
public String model;
public void setMake(String make)
{
this.make = make;
}
public String getMake()
{
return make;
}
}
AutomobileTest 类也要进行更新,以便使用新的访问方法,如清单15所示。
清单 15. AutomobileTest 类
public class AutomobileTest
{
public void race()
{
Automobilecar1 = new Automobile();
car1.setMake("Austin Healy");
car1.model= "Sprite";
// ...
}
}
改变方法的签名
本文介绍的最后一个重构方法也是最难以使用的方法:Change Method Signature(改变方法的签名)。这种方法的功能显而易见——改变方法的参数、可见性以及返回值的类型。而进行这样的改变对于调用这个方法的其他方法或者代码会产生什么影响,就不是那么显而易见了。这么也没有什么魔方。如果代码的改变在被重构的方法内部引发了问题——变量未定义,或者类型不匹配——重构操作将对这些问题进行标记。您可以选择是接受重构,稍后改正这些问题,还是取消重构。如果这种重构在其他的方法中引发问题,就直接忽略这些问题,您必须在重构之后亲自修改。
为澄清这一点,考虑清单16中列出的类和方法。
清单 16. MethodSigExample 类
public class MethodSigExample
{
public int test(String s, int i)
{
int x = i + s.length();
return x;
}
}
上面这个类中的 test() 方法被另一个类中的方法调用,如清单17所示。
清单 17. callTest 方法
public void callTest()
{
MethodSigExample eg = new MethodSigExample();
int r = eg.test("hello", 10);
}
在第一个类中高亮选中 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() 的代码就变成下面的样子:
public void callTest()
{
MethodSigExample eg = new MethodSigExample();
int r = eg.test("hello", 10, "world");
}
在这场有关 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 网站。