本文用eclipsede自动重构功能对一个程序实例进行重构,目de是探索Eclipse自动重构可以在多大程度上辅助重构这个过程.程序实例使用《Refactoring:Improving the Design of Existing Code》一书中de例子.
Eclipsede自动重构功能能够很好地支持各种程序元素de重命名,并自动更新相关de引用.Eclipse能够支持方法、字段在类之间移动,并自动更新引用.Eclipse较好地支持内联字段、函数de更新替换.Eclipse较好地支持抽取方法、变量等程序元素.
重构de过程是一个不断尝试和探索de过程.Eclipsede重构支持撤销和重做,并且能够预览重构结果,这些是很实用de功能.
Eclipsede重命名、抽取方法、移动、内联功能、更改方法特征符等相关代码结构级别de重构方法,是比较成熟同时也值得使用de功能.至于设计结构上de重构,eclipse还不能很好地支持.但是作者相信,自动重构de理念应该是”工具辅助下de重构工作”,人仍然承担大部分重构工作.
一、预备工作
本文使用《Refactoring:Improving the Design of Existing Code》一书第一章de例子.重构前de相关代码及每一步重构后de相关代码见附件.读者最好配合《Refactoring:Improving the Design of Existing Code》一书阅读本文.
Eclipse使用如下版本:

同时安装了中文语言包.
二、重构第一步:分解并重组statement()
目de:
1、 把statement()函数中deswich语句提炼到独立de函数amountFor()中.
2、 修改amountFor()参数命名
重构方法:
Extract Method
Rename Method
方法:
1、选中swich语句de相关代码块,在右键菜单中选择”重构/抽取方法”,出现参数对话框.Eclipse自动分析相关代码块中de局部变量,找到了两个局部变量:each和thisAmount.其中,each只是在相关代码块中被读取,但thisAmount会在相关代码块中被修改.按照重构Extract Method总结出来de规则,应该把each当作抽取函数de参数、thisAmount当作抽取函数de返回值.然而Eclipse并不做区分,直接把这两个变量当作抽取新方法de参数,如图.

我de目de是把在抽取函数中不会被修改deeach作为参数;会被修改dethisAmount作为返回值.解决de办法是,把 double thisAmount = 0; 这行相关代码移到switch语句de上面,变成这样:
double thisAmount = 0;
switch(each.getMovie().getPriceCode()){
case Movie.REGULAR:
thisAmount = 2;
if(each.getDaysRented()>2)
thisAmount = (each.getDaysRented()-2)*1.5;
break;
case Movie.NEW_RELEASE:
thisAmount = each.getDaysRented()*3;
break;
case Movie.CHILDRENS:
thisAmount = 1.5;
if(each.getDaysRented()>3)
thisAmount = (each.getDaysRented()-3)*1.5;
break;
}
选中这段相关代码,在右键菜单中选择”重构/抽取方法”,eclipse这次变得聪明点了,如图.

选择”预览”按钮预先查看重构后de结果,符合我最初de目de.

选择”确定”按钮,重构后de相关代码片断如下:
public String statement() {
double totalAmount = 0;
int frequentRenterPoints = 0;
Enumeration rentals = _rentals.elements();
String result = “Rental Record for ” getName() ” “;
while(rentals.hasMoreElements()){
Rental each = (Rental)rentals.nextElement();
double thisAmount = amountFor(each);
frequentRenterPoints ;
if((each.getMovie().getPriceCode())==Movie.NEW_RELEASE &&each.getDaysRented()>1)
frequentRenterPoints ;
result = ” ” each.getMovie().getTitle() ” ” String.valueOf(thisAmount) ” “;
totalAmount = thisAmount;
}
result = “Amount owed is ” String.valueOf(totalAmount) ” “;
result = “You earned ” String.valueOf(frequentRenterPoints) ” frequent renter points”;
return result;
}
/**
* @param each
* @return
*/
private double amountFor(Rental each) {
double thisAmount = 0;
switch(each.getMovie().getPriceCode()){
case Movie.REGULAR:
thisAmount = 2;
if(each.getDaysRented()>2)
thisAmount = (each.getDaysRented()-2)*1.5;
break;
case Movie.NEW_RELEASE:
thisAmount = each.getDaysRented()*3;
break;
case Movie.CHILDRENS:
thisAmount = 1.5;
if(each.getDaysRented()>3)
thisAmount = (each.getDaysRented()-3)*1.5;
break;
}
return thisAmount;
}
2、选中amountFor()de参数each,在右键菜单中选择”重构/重命名”,在对话框中输入新de名称:aRental,选择确定,amountFor()中所有eachde引用全部被替换成新de名称.用同样de办法修改amountFor()中de局部变量thisAmount为result.重构后deamountFor()相关代码如下:
/**
* @param aRental
* @return
*/
private double amountFor(Rental aRental) {
double result = 0;
switch(aRental.getMovie().getPriceCode()){
case Movie.REGULAR:
result = 2;
if(aRental.getDaysRented()>2)
result = (aRental.getDaysRented()-2)*1.5;
break;
case Movie.NEW_RELEASE:
result = aRental.getDaysRented()*3;
break;
case Movie.CHILDRENS:
result = 1.5;
if(aRental.getDaysRented()>3)
result = (aRental.getDaysRented()-3)*1.5;
break;
}
return result;
}
三、重构第二步:搬移”金额计算”相关代码
目de:
1、 将函数amountFor()转移到Rental类中,并更名为getCharge().
2、 更新并替换所有对amountFor()de引用.
重构方法:
Move Method
Change Method signatrue
Inline Method
Inline Temp
方法:
1、选中函数amountFor()de定义,在右键菜单中选择”重构/移动”,显示参数设置对话框.把新方法名改成getCharge.按下”确定”按钮,Customer Class中deamountFor()函数被移动到Rental Class中,并更名为:getCharge().

同时eclipse自动在CustomerdeamountFor()函数中添加一行对新函数de”委托”相关代码:
private double amountFor(Rental aRental) {
return aRental.getCharge();
}
这行相关代码会产生编译错误,原因是amountFor()deprivate型被传递到了新de方法中:
/**
* @param this
* @return
*/
private double getCharge() {
……
}
2、继续重构!选中getCharge()方法,在右键菜单中选择”重构/更改方法特征符”,弹出参数选择对话框,把访问修饰符从private改成public.Eclipsede编译错误提示自动消失.

3、回到Customer类,把所有对amountFor()引用de地方替换成直接对getCharge()de引用.选中Customer类de函数amountFor(Rental aRental),在右键菜单中选择”重构/内联”,出现参数选择对话框.

选择”确认”按钮,引用amountFor()de地方被替换成对getCharge()de引用.
public String statement() {
……
double thisAmount = each.getCharge();
……
}
4、除去临时变量thisAmount.
选中变量thisAmount,在右键菜单中选择”重构/内联”,重构预览窗口如下,可见达到了重构de目de.按下”确认”按钮重构相关代码.

statement()相关代码:
public String statement() {
double totalAmount = 0; // 总消费金额
int frequentRenterPoints = 0; // 常客积点
Enumeration rentals = _rentals.elements();
String result = “Rental Record for ” getName() ” “;
while(rentals.hasMoreElements()){
Rental each = (Rental)rentals.nextElement(); //取得一笔租借记录
// add frequent renter points(累加 常客积点)
frequentRenterPoints ;
// add bouns for a two day new release rental
if((each.getMovie().getPriceCode())==Movie.NEW_RELEASE && each.getDaysRented()>1)
frequentRenterPoints ;
// show figures for this rental(显示此笔租借数据)
result = ” ” each.getMovie().getTitle() ” ”
String.valueOf(each.getCharge()) ” “;
totalAmount = each.getCharge();
}
// add footer lines(结尾打印)
result = “Amount owed is ” String.valueOf(totalAmount) ” “;
result = “You earned ” String.valueOf(frequentRenterPoints) ” frequent renter points”;
return result;
}
四、重构第三步:提炼”常客积点计算”相关代码
目de:提取”常客积点计算”相关代码并放在Rental类中,”常客积点计算”相关代码如下.
public String statement() {
……
// add frequent renter points
frequentRenterPoints ;
// add bouns for a two day new release rental
if((each.getMovie().getPriceCode())==Movie.NEW_RELEASE && each.getDaysRented()>1)
frequentRenterPoints ;
……
}
重构后de相关代码如下:
frequentRenterPoints = each.getFrequentRenterPoints();
重构方法:
Extract Method
Move Method
Change Method signatrue
Inline Method
方法:
1、 首先,抽取相关代码到独立de函数中.
用”抽取方法”重构相关代码,函数名:getFrequentRenterPoints.很遗憾,eclipsede不能生成诸如:frequentRenterPoints = getFrequentRenterPoints(Rental aRental); de相关代码.原因是执行自增操作de局部变量frequentRenterPoints要出现在等式右边,因此抽取函数getFrequentRenterPoints()一定要把frequentRenterPoints作为参数.手工修改函数和对函数de引用,重构后de相关代码如下:
public String statement() {
……
while(rentals.hasMoreElements()){
……
frequentRenterPoints = getFrequentRenterPoints(each);
……
}
……
}
/**
* @param each
* @return
*/
private int getFrequentRenterPoints(Rental each) {
if((each.getMovie().getPriceCode())==Movie.NEW_RELEASE && each.getDaysRented()>1)
return 2;
else
return 1;
}
2、 把getFrequentRenterPoints()移动到Rental类中.
3、 对getFrequentRenterPoints()”更改方法特征符”为public.
4、 对Customerde函数getFrequentRenterPoints()执行内联操作,重构目标完成.
五、重构第四步:去除临时变量(totalAmount和frequentRenterPoints)
目de:去除临时变量(totalAmount和frequentRenterPoints)
方法:
1、 分析totalAmount和frequentRenterPointsde定义和引用结构如下:
// 声明和定义
double totalAmount = 0;
int frequentRenterPoints = 0;
……
// 在循环中修改
while(rentals.hasMoreElements()){
……
frequentRenterPoints = each.getFrequentRenterPoints();
……
totalAmount = each.getCharge();
……
}
……
// 在循环外使用
result = “Amount owed is ” String.valueOf(totalAmount) ” “;
result = “You earned ” String.valueOf(frequentRenterPoints) ” frequent renter points”;
……
上述两个变量在循环体外面定义和使用,在循环中被修改,运用Replace Temp with Query方法去除这两个临时变量是一项稍微复杂de重构.很遗憾,eclipse目前不支持这样de重构.
2、手工修改相关代码.
六、重构第五步:运用多态取代与价格相关de条件逻辑
目de:
1、 把Rental类中de函数getCharge()移动到Movie类中.
2、 把Rental类中de函数getFrequentRenterPoints()移动到Movie类中.
重构方法:
Move Method
Inline Method
方法:
1、 选中Rental类中de函数getCharge(),右键菜单选中”重构/移动”,eclipse提示找不到接收者,不能移动.原因在于这行语句:
switch(getMovie().getPriceCode()){//取得影片出租价格
选中getMovie(),右键菜单选中”重构/内联”,确定后相关代码成为:
switch(_movie.getPriceCode()){ //取得影片出租价格
选中getCharge(),执行”重构/移动”后,函数被移动到Movie类中.然而这只是部分达成了重构目de,我发现,移动后de相关代码把Rental作为参数传给了getCharge(),手工修改一下,相关代码变成:
class Movie ……
/**
* @param this
* @return
*/
public double getCharge(int _daysRented) {
double result = 0;
switch(getPriceCode()){ //取得影片出租价格
case Movie.REGULAR: // 普通片
result = 2;
if(_daysRented>2)
result = (_daysRented-2)*1.5;
break;
case Movie.NEW_RELEASE: // 新片
result = _daysRented*3;
break;
case Movie.CHILDRENS: // 儿童片
result = 1.5;
if(_daysRented>3)
result = (_daysRented-3)*1.5;
break;
}
return result;
}
class Rental……
/**
* @param this
* @return
*/
public double getCharge() {
return _movie.getCharge(_daysRented);
}
2、用同样de步骤处理getFrequentRenterPoints(),重构后de相关代码:
class Movie ……
/**
* @param frequentRenterPoints
* @param this
* @return
*/
public int getFrequentRenterPoints(int daysRented) {
if((getPriceCode())==Movie.NEW_RELEASE && daysRented>1)
return 2;
else
return 1;
}
class Rental……
/**
* @param frequentRenterPoints
* @param this
* @return
*/
public int getFrequentRenterPoints(int daysRented) {
if((getPriceCode())==Movie.NEW_RELEASE && daysRented>1)
return 2;
else
return 1;
}
七、重构第六步:终于……我来到继承
目de:对switch语句引入state模式.
方法:
很遗憾,不得不在这里提前结束eclipsede自动重构之旅.Eclipse几乎不能做结构上de重构.也许Martin Fowler在书中呼唤de自动重构工具止于”工具辅助下de重构工作”这一理念.艺术是人类de专利,编程艺术de梦想将持续下去.
感兴趣de读者可以查看手工重构de最后一步相关代码.将重构进行到底!
附录:eclipse支持de重构方法(摘自eclipse中文帮助)
名称功能
撤销执行上一次重构de”撤销”.只要除了重构之外尚未执行任何其它源更改,重构撤销缓冲区就有效.
重做执行上一次撤销重构de”重做”.只要除了重构之外尚未执行任何其它源更改,重构撤销/重做缓冲区就有效.
重命名 启动”重命名”重构对话框:重命名所选择de元素,并更正对元素de所有引用(如果启用了de话)(还在其它文件中).可用于:方法、字段、局部变量、方法参数、类型、编译单元、包、源文件夹和项目,以及解析为这些元素类型中de其中一种de文本选择部分.
移动 启动”移动”重构对话框:移动所选择de元素,并更正对元素de所有引用(如果启用了de话)(还在其它文件中).适用于:一个实例方法(可以将它移至某个组件)、一个或多个静态方法、静态字段、类型、编译单元、包、源文件夹和项目,以及解析为这些元素类型中de其中一种de文本选择部分.
更改方法特征符启动”更改方法特征符”重构对话框.更改参数名称、参数类型和参数顺序,并更新对相应方法de所有引用.此外,可以除去或添加参数,并且可以更改方法返回类型和它de可视性.可以将此重构应用于方法或解析为方法de文本选择.
将匿名类转换为嵌套类启动”将匿名类转换为嵌套类”重构对话框.帮助您将匿名内部类转换为成员类.可以将此重构应用于匿名内部类.
将嵌套类型转换成顶层启动”将嵌套类型转换为顶层类型”重构对话框.为所选成员类型创建新de Java 编译单元,并根据需要更新所有引用.对于非静态成员类型,将添加字段以允许访问先前de外围实例.可以将此重构应用于成员类型或解析为成员类型de文本.
下推启动”下推”重构对话框.将一组方法和字段从一个类移至它de子类.可以将此重构应用于在同一个类型中声明de一个或多个方法和字段或者字段或方法内de文本选择.
上拉启动”上拉”重构型中声明de一个或多个方法、字段和成员类型,也可以应用于字段、方法或成员类型内de文本选择.向导.将字段或方法移至其声明类de超类或者(对于方法)将方法声明为超类中de抽象类.可以将此重构应用于在同一个类
抽取接口启动”抽取接口”重构对话框.使用一组方法创建新接口并使选择de类实现该接口,并尽可能地将对该类de引用更改为对新接口de引用(可选).可以将此重构应用于类型.
尽可能使用超类型启动”尽可能使用超类型”对话框.将某个类型de出现替换为它de其中一个超类型,在执行此替换之前,需要标识所有有可能进行此替换de位置.此重构可用于类型.
内联启动”内联”重构对话框.内联局部变量、方法或常量.此重构可用于方法、静态终态字段和解析为方法、静态终态字段或局部变量de文本选择.
抽取方法启动”抽取方法”重构对话框.创建一个包含当前所选择de语句或表达式de新方法,并将选择替换为对新方法de引用.可以使用编辑菜单中de扩大选择至以获取有效de选择范围.此功能对于清理冗长、杂乱或过于复杂de方法是很有用de.
抽取局部变量启动”抽取变量”重构对话框.创建为当前所选择de表达式指定de新变量,并将选择替换为对新变量de引用.此重构可用于解析为局部变量de文本选择.可以使用编辑菜单中de扩大选择至以获取有效de选择范围.
抽取常量启动”抽取常量”重构对话框.从所选表达式创建静态终态字段并替换字段引用,并且可以选择重写同一表达式de其它出现位置.此重构可用于静态终态字段和解析为静态终态字段de文本选择.
将局部变量转换为字段启动”将局部变量转换为字段”重构对话框.将局部变量转换为字段.如果该变量是在创建时初始化de,则此操作将把初始化移至新字段de声明或类de构造函数.此重构可用于解析为局部变量de文本选择.
封装字段启动”自封装字段”重构对话框.将对字段de所有引用替换为 getting 和 setting 方法.它适用于所选择de字段或解析为字段de文本选择.