您的位置:首页 > 编程语言 > Java开发

任何人都可以重构---使用 Eclipse 自动重构特性的方法与原因 选择自 shanyou 的 Blog

2004-12-27 09:19 399 查看
 
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方法。
还有几个重构工具并不能完全归入这三个种类,特别是ChangeMethodSignature,不过在本文中还是将这个工具归入第三类。除了这种例外情况以外,本文下面几节都是按照上面的顺序来讨论Eclipse重构工具的。

物理重组与重命名
显然,您即便没有特别的工具,也可以在文件系统中重命名文件或者是移动文件,但是如果操作对象是Java源代码文件,您就需要编辑很多文件,更新其中的
import
package
语句。与此类似,用某种文本编辑器的搜索与替换功能也可以很容易地给类、方法和变量重新命名,但是这样做的时候必须十分小心,因为不同的类可能具有名称相似的方法或者变量;要是从头到尾检查项目中所有的文件,来保证每个东西的标识和修改的正确性,那可真够乏味的。

Eclipse的Rename和Move工具能够十分聪明地在整个项目中完成这样的修改,而不需要用户的干涉。这是因为Eclipse可以理解代码的语义,从而能够识别出对某个特定方法、变量或者类名称的引用。简化这一任务有助于确保方法、变量和类的名称能够清晰地指示其用途。

我们经常可以发现代码的名字不恰当或者令人容易误解,这是因为代码与最初设计的功能有所不同。比方说,某个用来在文件中查找特定单词的程序也许会扩展为在Web页面中通过URL获取
InputStream
的操作。如果这一输入流最初叫做
file
,那么就应该修改它的名字,以便能反映其新增的更加一般的特性,比方说
sourceStream
。开发人员经常无法成功地修改这些名称,因为这个过程是十分混乱和乏味的。这当然也会把下一个不得不对这些类进行操作的开发人员弄糊涂。

要对某个Java元素进行重命名,只需要简单地从PackageExplorer视图中点击这个元素,或者从Java源代码文件中选中这个元素,然后选择菜单项Refactor>Rename。在对话框中输入新的名称,然后选择是否需要Eclipse也改变对这个名称的引用。实际显示出来的确切内容与您所选元素的类型有关。比方说,如果选择的属性具有getter和setter方法,那么也就可以同时更新这些方法的名称,以反映新的属性。图1显示了一个简单的例子。

图1.重命名一个局部变量



就像所有的Eclipse重构操作一样,当您指定了全部用来执行重构的必要信息之后,您就可以点击Preview按钮,然后在一个对话框中对比Eclipse打算进行哪些变更,您可以分别否决或者确认每一个受到影响的文件中的每一项变更。如果您对于Eclipse正确执行变更的能力有信心的话,您可以只按下OK按钮。显然,如果您不确定重构到底做了什么事情,您就会想先预览一下,但是对于Rename和Move这样简单的重构而言,通常没有必要预览。

Move操作与Rename十分相似:您选择某个Java元素(通常是一个类),为其指定一个新位置,并定义是否需要更新引用。然后,您可以选择Preview检查变更情况,或者选择OK立即执行重构,如图2所示。

图2.将类从一个包移到另一个包



在某些平台上(特别是Windows),您还可以在PackageExplorer视图中通过简单拖放的方法将类从一个包或者文件夹中移到另一个包或文件夹中。所有的引用都会自动更新。

重新定义类的关系
Eclipse中有大量的重构工具,使您能够自动改变类的关系。这些重构工具并没有Eclipse提供的其他工具那么常用,但是很有价值,因为它们能够执行非常复杂的任务。可以说,当它们用得上的时候,就会非常有用。

提升匿名类与嵌套类
ConvertAnonymousClass(转换匿名类)和ConvertNestedType(转换嵌套类)这两种重构方法比较相似,它们都将某个类从其当前范围移动到包含这个类的范围上。

匿名类是一种语法速写标记,使您能够在需要实现某个抽象类或者接口的地方创建一个类的实例,而不需要显式提供类的名称。比如在创建用户界面中的监听器时,就经常用到匿名类。在清单1中,假设Bag是在其他地方定义的一个接口,其中声明了两个方法,
get()
set()

清单1.Bag类
publicclassBagExample
{
voidprocessMessage(Stringmsg)
{
Bagbag=newBag()
{
Objecto;
publicObjectget()
{
returno;
}
publicvoidset(Objecto)
{
this.o=o;
}
};
bag.set(msg);
MessagePipepipe=newMessagePipe();
pipe.send(bag);
}
}
当匿名类变得很大,其中的代码难以阅读的时候,您就应该考虑将这个匿名类变成严格意义上的类;为了保持封装性(换句话说,就是将它隐藏起来,使得不必知道它的外部类不知道它),您应该将其变成嵌套类,而不是顶级类。您可以在这个匿名类的内部点击,然后选择Refactor>ConvertAnonymousClasstoNested就可以了。当出现确认对话框的时候,为这个类输入名称,比如
BagImpl
,然后选择Preview或者OK。这样,代码就变成了如清单2所示的情形。
清单2.经过重构的Bag类
publicclassBagExample
{
privatefinalclassBagImplimplementsBag
{
Objecto;
publicObjectget()
{
returno;
}
publicvoidset(Objecto)
{
this.o=o;
}
}

voidprocessMessage(Stringmsg)
{
Bagbag=newBagImpl();
bag.set(msg);
MessagePipepipe=newMessagePipe();
pipe.send(bag);
}
}
当您想让其他的类使用某个嵌套类时,ConvertNestedTypetoTopLevel就很有用了。比方说,您可以在一个类中使用值对象,就像上面的
BagImpl
类那样。如果您后来又决定应该在多个类之间共享这个数据,那么重构操作就能从这个嵌套类中创建新的类文件。您可以在源代码文件中高亮选中类名称(或者在Outline视图中点击类的名称),然后选择Refactor>ConvertNestedTypetoTopLevel,这样就实现了重构。

这种重构要求您为装入实例提供一个名字。重构工具也会提供建议的名称,比如
example
,您可以接受这个名字。这个名字的意思过一会儿就清楚了。点击OK之后,外层类
BagExample
就会变成清单3所示的样子。
清单3.经过重构的Bag类
publicclassBagExample
{
voidprocessMessage(Stringmsg)
{
Bagbag=newBagImpl(this);
bag.set(msg);
MessagePipepipe=newMessagePipe();
pipe.send(bag);
}
}
请注意,当一个类是嵌套类的时候,它可以访问其外层类的成员。为了保留这种功能,重构过程将一个装入类BagExample的实例放在前面那个嵌套类中。这就是之前要求您输入名称的实例变量。同时也创建了用于设置这个实例变量的构造函数。重构过程创建的新类BagImpl如清单4所示。
清单4.BagImpl类

finalclassBagImplimplementsBag
{
privatefinalBagExampleexample;
/**
*@paramBagExample
*/
BagImpl(BagExampleexample)
{
this.example=example;
//TODOAuto-generatedconstructorstub
}
Objecto;
publicObjectget()
{
returno;
}
publicvoidset(Objecto)
{
this.o=o;
}
}
如果您的情况与这个例子相同,不需要保留对
BagExample
的访问,您也可以很安全地删除这个实例变量与构造函数,将
BagExample
类中的代码改成缺省的无参数构造函数。

在类继承关系内移动成员
还有两个重构工具,PushDown和PullUp,分别实现将类方法或者属性从一个类移动到其子类或父类中。假设您有一个名为
Vehicle
的抽象类,其定义如清单5所示。
清单5.抽象的Vehicle类
publicabstractclassVehicle
{
protectedintpassengers;
protectedStringmotor;

publicintgetPassengers()
{
returnpassengers;
}
publicvoidsetPassengers(inti)
{
passengers=i;
}
publicStringgetMotor()
{
returnmotor;
}
publicvoidsetMotor(Stringstring)
{
motor=string;
}
}
您还有一个
Vehicle
的子类,类名为
Automobile
,如清单6所示。
清单6.Automobile类
publicclassAutomobileextendsVehicle
{
privateStringmake;
privateStringmodel;
publicStringgetMake()
{
returnmake;
}
publicStringgetModel()
{
returnmodel;
}
publicvoidsetMake(Stringstring)
{
make=string;
}
publicvoidsetModel(Stringstring)
{
model=string;
}
}
请注意,
Vehicle
有一个属性是
motor
。如果您知道您将永远只处理汽车,那么这样做就好了;但是如果您也允许出现划艇之类的东西,那么您就需要将
motor
属性从
Vehicle
类下放到
Automobile
类中。为此,您可以在Outline视图中选择
motor
,然后选择Refactor>PushDown

Eclipse还是挺聪明的,它知道您不可能总是单单移动某个属性本身,因此还提供了AddRequired按钮,不过在Eclipse2.1中,这个功能并不总是能正确地工作。您需要验证一下,看所有依赖于这个属性的方法是否都推到了下一层。在本例中,这样的方法有两个,即与
motor
相伴的getter和setter方法,如图3所示。

图3.加入所需的成员



在按过OK按钮之后,
motor
属性以及
getMotor()
setMotor()
方法就会移动到
Automobile
类中。清单7显示了在进行了这次重构之后
Automobile
类的情形。
清单7.经过重构的Automobile类

publicclassAutomobileextendsVehicle
{
privateStringmake;
privateStringmodel;
protectedStringmotor;
publicStringgetMake()
{
returnmake;
}
publicStringgetModel()
{
returnmodel;
}
publicvoidsetMake(Stringstring)
{
make=string;
}
publicvoidsetModel(Stringstring)
{
model=string;
}
publicStringgetMotor()
{
returnmotor;
}
publicvoidsetMotor(Stringstring)
{
motor=string;
}
}
PullUp重构与PushDown几乎相同,当然PullUp是将类成员从一个类中移到其父类中,而不是子类中。如果您稍后改变主意,决定还是把
motor
移回到
Vehicle
类中,那么您也许就会用到这种重构。同样需要提醒您,一定要确认您是否选择了所有必需的成员。

Automobile
类中具有成员motor,这意味着您如果创建另一个子类,比方说
Bus
,您就还需要将
motor
(及其相关方法)加入到
Bus
类中。有一种方法可以表示这种关系,即创建一个名为
Motorized
的接口,
Automobile
Bus
 都实现这个接口,但是
RowBoat
不实现。

创建
Motorized
接口最简单的方法是在
Automobile
上使用ExtractInterface重构。为此,您可以在Outline视图中选择
Automobile
,然后从菜单中选择Refactor>ExtractInterface。您可以在弹出的对话框中选择您希望在接口中包含哪些方法,如图4所示。

图4.提取Motorized接口



点击OK之后,接口就创建好了,如清单8所示。
清单8.Motorized接口
publicinterfaceMotorized
{
publicabstractStringgetMotor();
publicabstractvoidsetMotor(Stringstring);
}
同时,
Automobile
的类声明也变成了下面的样子:


publicclassAutomobileextendsVehicleimplementsMotorized
使用父类
本重构工具类型中最后一个是UserSupertypWherePossible。想象一个用来管理汽车细帐的应用程序。它自始至终都使用
Automobile
类型的对象。如果您想处理所有类型的交通工具,那么您就可以用这种重构将所有对
Automobile
的引用都变成对Vehicle的引用(参看图5)。如果您在代码中用
instanceof
操作执行了任何类型检查的话,您将需要决定在这些地方适用的是原先的类还是父类,然后选中第一个选项“Usetheselectedsupertypein'instanceof'expressions”。

图5.将Automobile改成其父类Vehicle



使用父类的需求在Java语言中经常出现,特别是在使用了FactoryMethod模式的情况下。这种模式的典型实现方式是创建一个抽象类,其中具有静态方法
create()
,这个方法返回的是实现了这个抽象类的一个具体对象。如果需创建的具体对象的类型依赖于实现的细节,而调用类对实现细节并不感兴趣的情况下,可以使用这一模式。

改变类内部的代码
最大一类重构是实现了类内部代码重组的重构方法。在所有的重构方法中,只有这类方法允许您引入或者移除中间变量,根据原有方法中的部分代码创建新方法,以及为属性创建getter和setter方法。

提取与内嵌
有一些重构方法是以Extract这个词开头的:ExtractMethod、ExtractLocalVariable以及ExtractConstants。第一个ExtractMethod的意思您可能已经猜到了,它根据您选中的代码创建新的方法。我们以清单8中那个类的
main()
方法为例。它首先取得命令行选项的值,如果有以-D开头的选项,就将其以名-值对的形式存储在一个
Properties
 对象中。
清单8.main()

importjava.util.Properties;
importjava.util.StringTokenizer;
publicclassStartApp
{
publicstaticvoidmain(String[]args)
{
Propertiesprops=newProperties();
for(inti=0;i<args.length;i++)
{
if(args.startsWith("-D"))
{
Strings=args[i].substring(2);
StringTokenizerst=newStringTokenizer(s,"=");
if(st.countTokens()==2)
{
props.setProperty(st.nextToken(),st.nextToken());
}
}
}
//continue...
}
}
将一部分代码从一个方法中取出并放进另一个方法中的原因主要有两种。第一种原因是这个方法太长,并且完成了两个以上逻辑上截然不同的操作。(我们不知道上面那个
main()
方法还要处理哪些东西,但是从现在掌握的证据来看,这不是从其中提取出一个方法的理由。)另一种原因是有一段逻辑上清晰的代码,这段代码可以被其他方法重用。比方说在某些时候,您发现自己在很多不同的方法中都重复编写了相同的几行代码。那就有可能是需要重构的原因了,不过除非真的需要重用这部分代码,否则您很可能并不会执行重构。

假设您还需要在另外一个地方解析名-值对,并将其放在
Properties
对象中,那么您可以将包含
StringTokenizer
声明和下面的
if
语句的这段代码抽取出来。为此,您可以高亮选中这段代码,然后从菜单中选择Refactor>ExtractMethod。您需要输入方法名称,这里输入
addProperty
,然后验证这个方法的两个参数,
Propertiesprop
Strings
。清单9显示由Eclipse提取了
addProp()
方法之后类的情况。
清单9.提取出来的addProp()

importjava.util.Properties;
importjava.util.StringTokenizer;
publicclassExtract
{
publicstaticvoidmain(String[]args)
{
Propertiesprops=newProperties();
for(inti=0;i<args.length;i++)
{
if(args[i].startsWith("-D"))
{
Strings=args[i].substring(2);
addProp(props,s);
}
}
}
privatestaticvoidaddProp(Propertiesprops,Strings)
{
StringTokenizerst=newStringTokenizer(s,"=");
if(st.countTokens()==2)
{
props.setProperty(st.nextToken(),st.nextToken());
}
}
}
ExtractLocalVariable重构取出一段被直接使用的表达式,然后将这个表达式首先赋值给一个局部变量。然后在原先使用那个表达式的地方使用这个变量。比方说,在上面的方法中,您可以高亮选中对
st.nextToken()
的第一次调用,然后选择Refactor>ExtractLocalVariable。您将被提示输入一个变量名称,这里输入
key
。请注意,这里有一个将被选中表达式所有出现的地方都替换成新变量的引用的选项。这个选项通常是适用的,但是对这里的
nextToken()
方法不适用,因为这个方法(显然)在每一次调用的时候都返回不同的值。确认这个选项未被选中。参见图6。

图6.不全部替换所选的表达式



接下来,在第二次调用
st.nextToken()
的地方重复进行重构,这一次调用的是一个新的局部变量
value
。清单10显示了这两次重构之后代码的情形。
清单10.重构之后的代码

privatestaticvoidaddProp(Propertiesprops,Strings)
{
StringTokenizerst=newStringTokenizer(s,"=");
if(st.countTokens()==2)
{
Stringkey=st.nextToken();
Stringvalue=st.nextToken();
props.setProperty(key,value);
}
}
用这种方式引入变量有几点好处。首先,通过为表达式提供有意义的名称,可以使得代码执行的任务更加清晰。第二,代码调试变得更容易,因为我们可以很容易地检查表达式返回的值。最后,在可以用一个变量替换同一表达式的多个实例的情况下,效率将大大提高。

ExtractConstant与ExtractLocalVariable相似,但是您必须选择静态常量表达式,重构工具将会把它转换成静态的final常量。这在将硬编码的数字和字符串从代码中去除的时候非常有用。比方说,在上面的代码中我们用“-D”这一命令行选项来定义名-值对。先将“-D”高亮选中,选择Refactor>ExtractConstant,然后输入DEFINE作为常量的名称。重构之后的代码如清单11所示:
清单11.重构之后的代码

publicclassExtract
{
privatestaticfinalStringDEFINE="-D";
publicstaticvoidmain(String[]args)
{
Propertiesprops=newProperties();
for(inti=0;i<args.length;i++)
{
if(args[i].startsWith(DEFINE))
{
Strings=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属性,而用publicsetter和getter方法来访问属性的原因。这些方法可以用两种不同的方式自动生成。

第一种生成这些方法的方式是使用Source>GenerateGetterandSetter菜单。这将会显示一个对话框,其中包含所有尚未存在的getter和setter方法。不过因为这种方式没有用新方法更新对这些属性的引用,所以并不算是重构;必要的时候,您必须自己完成更新引用的工作。这种方式可以节约很多时间,但是最好是在一开始创建类的时候,或者是向类中加入新属性的时候使用,因为这些时候还不存在对属性的引用,所以不需要再修改其他代码。

第二种生成getter和setter方法的方式是选中某个属性,然后从菜单中选择Refactor>EncapsulateField。这种方式一次只能为一个属性生成getter和setter方法,不过它与Source>GenerateGetterandSetter相反,可以将对这个属性的引用改变成对新方法的调用。

例如,我们可以先创建一个新的简版
Automobile
类,如清单12所示。
清单12.简单的Automobile类

publicclassAutomobileextendsVehicle
{
publicStringmake;
publicStringmodel;
}
接下来,创建一个类实例化了
Automobile
的类,并直接访问
make
属性,如清单13所示。
清单13.实例化Automobile

publicclassAutomobileTest
{
publicvoidrace()
{
Automobilecar1=newAutomobile();
car1.make="AustinHealy";
car1.model="Sprite";
//...
}
}
现在封装
make
属性。先高亮选中属性名称,然后选择Refactor>EncapsulateField。在弹出的对话框中输入getter和setter方法的名称——如您所料,缺省的方法名称分别是getMake()和setMake()。您也可以选择与这个属性处在同一个类中的方法是继续直接访问该属性,还是像其他类那样改用这些访问方法。(有一些人非常倾向于使用这两种方式的某一种,不过碰巧在这种情况下您选择哪一种方式都没有区别,因为
Automobile
中没有对
make
属性的引用。)

图7.封装属性



点击OK之后,Automobile类中的
make
属性就变成了私有属性,也同时具有了
getMake()
setMake()
方法。
清单14.经过重构的Automobile类

publicclassAutomobileextendsVehicle
{
privateStringmake;
publicStringmodel;

publicvoidsetMake(Stringmake)
{
this.make=make;
}

publicStringgetMake()
{
returnmake;
}
}
AutomobileTest
类也要进行更新,以便使用新的访问方法,如清单15所示。
清单15.AutomobileTest类

publicclassAutomobileTest
{
publicvoidrace()
{
Automobilecar1=newAutomobile();
car1.setMake("AustinHealy");
car1.model="Sprite";
//...
}
}
改变方法的签名
本文介绍的最后一个重构方法也是最难以使用的方法:ChangeMethodSignature(改变方法的签名)。这种方法的功能显而易见——改变方法的参数、可见性以及返回值的类型。而进行这样的改变对于调用这个方法的其他方法或者代码会产生什么影响,就不是那么显而易见了。这么也没有什么魔方。如果代码的改变在被重构的方法内部引发了问题——变量未定义,或者类型不匹配——重构操作将对这些问题进行标记。您可以选择是接受重构,稍后改正这些问题,还是取消重构。如果这种重构在其他的方法中引发问题,就直接忽略这些问题,您必须在重构之后亲自修改。

为澄清这一点,考虑清单16中列出的类和方法。
清单16.MethodSigExample类

publicclassMethodSigExample
{
publicinttest(Strings,inti)
{
intx=i+s.length();
returnx;
}
}
上面这个类中的
test()
方法被另一个类中的方法调用,如清单17所示。
清单17.callTest方法

publicvoidcallTest()
{
MethodSigExampleeg=newMethodSigExample();
intr=eg.test("hello",10);
}
在第一个类中高亮选中
test
,然后选择Refactor>ChangeMethodSignature。您将看到如图8所示的对话框。

图8.ChangeMethodSignature选项



第一个选项是改变该方法的可见性。在本例中,将其改变为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
,那么可以先从要进行重构的方法中删除对它的引用。这样删除参数的过程就更加顺利了。

最后一件需要解释的事情是DefaultValue选项。这一选项值仅适用于将参数加入方法签名中的情况。比方说,如果我们加入了一个类型为
String
的参数,参数名为
n
,其缺省值为
world
,那么在
callTest()
方法中调用
test()
的代码就变成下面的样子:


publicvoidcallTest()
{
MethodSigExampleeg=newMethodSigExample();
intr=eg.test("hello",10,"world");
}
在这场有关ChangeMethodSignature重构的看似可怕的讨论中,我们并没有隐藏其中的问题,但却一直没有提到,这种重构其实是非常强大的工具,它可以节约很多时间,通常您必须进行仔细的计划才能成功地使用它。

结束语
Eclipse提供的工具使重构变得简单,熟悉这些工具将有助于您提高效率。敏捷开发方法采用迭代方式增加程序特性,因此需要依赖于重构技术来改变和扩展程序的设计。但即便您并没有使用要求进行正式重构的方法,Eclipse的重构工具还是可以在进行一般的代码修改时提供节约时间的方法。如果您花些时间熟悉这些工具,那么当出现可以利用它们的情况时,您就能意识到所花费的时间是值得的。

参考资料

书籍

有关重构的核心著作是[i]Refactoring:ImprovingtheDesignofExistingCode,作者MartinFowler、KentBeck、JohnBrant、WilliamOpdyke和DonRoberts(Addison-Wesley,1999年)。

重构是一种正在发展的方法,在EclipseInAction:AGuideforJavaDevelopers(Manning,2003年)一书中,作者DavidGallardo,EdBurnette以及RobertMcGovern从在Eclipse中设计和开发项目的角度讨论了这一话题。

模式(如本文中提到的FactoryMethod模式)是理解和讨论面向对象设计的重要工具。这方面的经典著作是DesignPatterns:ElementsofReusableObject-OrientedSoftware,作者为ErichGamma、RichardHelm、RalphJohnson和JohnVlissides(Addison-Wesley,1995年)。

DesignPatterns中的例子是用C++写成的,这对于Java程序员是不小的障碍;MarkGrand所著的PatternsinJava,VolumeOne:ACatalogofReusableDesignPatternsIllustratedwithUML(Wiley,1998年)将模式翻译成了Java语言。

有关敏捷编程的一个变种,请参看KentBeck所著的ExtremeProgrammingExplained:EmbraceChange(Addison-Wesley,1999年)
Web站点

MartinFowler的个人网站是Web上的重构技术中心。

有关用JUnit进行单元测试的更多信息,请访问JUnit网站。

RefactoringinVS.NETWhidbey.PDF
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
相关文章推荐