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

重构 改善既有代码的设计—— 重新组织方法

2016-09-13 16:41 399 查看
一.Extract Method(提炼方法)

1.动机:如果函数过长或代码段需要注释才能理解,就将这段代码放到独立函数中;有几个原因造成我喜欢简短而命名良好的的方法:

A.函数粒度小,复用几率高

B.函数粒度小,复写容易

C.函数粒度小,使高层函数读起来向一系列注释

   常常有人在问,一个方法长度多长才算合适。在我看来,方法多长不是问题,关键是方法名和方法体之间的语义距离。如果提炼可以强化方法的清晰度,即使提炼出来的方法的      方法名比方法体还长也无所谓。

2.做法

A.创造新的目标函数,根据函数功能命名;即使提炼的代码很简单, 只要目标函数的名称能更好的昭示代码意图,也应该提炼它。如果你想不出一个更有意义的名称,那就别动它;

B.将提炼的代码从源函数中复制到目标函数;

C.仔细检查提炼出的代码,查看其中是否引用了作用域仅限于源函数的变量(包括局部变量和源函数参数);

D.检查是否有仅用于被提炼代码段的临时变量,如果有,在目标函数中将他们声明为临时变量;

E.检查被提炼函数,查看是否有局部变量被其改变;如果有一个临时变量被改变,看看是否可以将被提炼代码段处理成查询,将返回值赋给相关变量;如果有多个临时变量被改变,就需要先使用Split Temporary Variable,然后再提炼;也可以先使用Replace Temp with Query消灭临时变量;

F.将被提炼代码段中需要的变量当作参数传给目标函数;

G.处理完所有变量后,编译;

H.在源函数中,将被提炼代码段替换成目标函数的引用;如果你将被提炼代码段的任何变量都放到目标函数中,请检查它们原来的声明是否还在,如果在的话,可以删除了;

I.编译,测试;

3.范例 

A无局部变量

private void printOwing(Enumeration enumeration){
double outStanding = 0.0;

System.out.println("*****************************");
System.out.println("*******Custorm Owes**********");
System.out.println("*****************************");

while (enumeration.hasMoreElements()){
Order order = (Order)enumeration.nextElement();
outStanding += order.getNum();
}

System.out.println("name:" + name);
System.out.println("outStanding :" + outStanding);
}



提炼打印横幅的代码段,只要剪切复制即可:

private void printOwing(Enumeration enumeration){
double outStanding = 0.0;

printBanner();

while (enumeration.hasMoreElements()){
Order order = (Order)enumeration.nextElement();
outStanding += order.getNum();
}

System.out.println("name:" + name);
System.out.println("outStanding :" + outStanding);
}

private void printBanner(){
System.out.println("*****************************");
System.out.println("*******Custorm Owes**********");
System.out.println("*****************************");
}

B.有局部变量;问题点在意源函数的参数和源函数声明的临时变量,局部变量的作用域仅限于源函数,所以提炼代码段时需要花功夫处理这些临时变量;局部变量最简单的情况是被提炼代码只是读取这些值,而不用修改它们,这种情况下可以将局部变量当作参数传递给目标函数;

private void printOwing(Enumeration enumeration){
double outStanding = 0.0;

System.out.println("*****************************");
System.out.println("*******Custorm Owes**********");
System.out.println("*****************************");

while (enumeration.hasMoreElements()){
Order order = (Order)enumeration.nextElement();
outStanding += order.getNum();
}

System.out.println("name:" + name);
System.out.println("outStanding :" + outStanding);
}

将打印详细信息的代码段提炼到目标函数中:


private void printOwing(Enumeration enumeration){
double outStanding = 0.0;
printBanner();
while (enumeration.hasMoreElements()){
Order order = (Order)enumeration.nextElement();
outStanding += order.getNum();
}
printDetail(outStanding);
}

private void printDetail(double outStanding){
System.out.println("name:" + name);
System.out.println("outStanding :" + outStanding);
}


C.对局部变量赋值

如果被提炼的代码段对局部变量赋值,就略微复杂;此时可分两种情况:1.被赋值的临时变量只在目标函数中使用,此时可以将声明直接放到目标函数中。2.被提炼的代码段之外的代码也使用了这个变量,这也分两种情况:1.如果这个变量在被提炼代码段后使用,直接在代码段中修改就好了;如果被提炼的代码段后的代码也使用了这个变量,就需要让目标函数返回该变量修改后的值;代码如下:

private void printOwing(Enumeration enumeration){
double outStanding = 0.0;
printBanner();
while (enumeration.hasMoreElements()){
Order order = (Order)enumeration.nextElement();
outStanding += order.getNum();
}
printDetail(outStanding);
}

现在把计算代码提炼出来:

 

private void printOwing(Enumeration enumeration){
printBanner();
double outStanding = getOutStanding(enumeration);
printDetail(outStanding);
}

private double getOutStanding(Enumeration enumeration){
double outStanding = 0.0;
while (enumeration.hasMoreElements()){
Order order = (Order)enumeration.nextElement();
outStanding += order.getNum();
}
return outStanding;
}


二.Inline Method(内联方法)

一个函数的本体与名称同样清晰易懂,在函数调用点插入函数本体,然后移除该函数


private int getRating(){
return (moreThanFiveLaterDeliveries()) ? 3:2;
}

private boolean moreThanFiveLaterDeliveries(){
return numberOfLaterDeliveries > 5;
}

private int getRating(){
return (numberOfLaterDeliveries > 5) ?3:2;
}


1.动机:

A.函数内部代码和函数名称同样清晰可读,此时应该去除函数名称,直接使用其中的代码;

B.有一群不甚合理的函数,此时可以把他们全部内联到一个大型函数中,再从中提炼出小函数;

C.如果使用了太多的间接层,使得系统中所有函数都似乎是对其他函数的简单委托,造成我在这些委托之间晕头转向,此时可以使用内联方法;当然间接层有其价值,但并    非所有间接层都有意义,找出那些无意义的,然后直接删除;

2.做法

A.检查函数,确定它不具有多态性(如果子类继承了该函数,就不要内联,因为子类无法继承一个不存在的函数);

B.找出该函数的所有调用点;

C.将这个函数的所有调用点替换成函数本体

D.编译、测试

E.删除该函数的定义;

三.Inline Temp(内联临时变量)

如果一个临时变量只被一个简单表达式赋值一次,而它妨碍了其他重构手法,将所有对该变量的操作,替换成对它赋值的那个表达式本身。

1.动机

A.Inline Temp多作为Replace Temp with Query的一部分使用的,所以真正的动机在后者;

B.Inline Temp单独使用的情况是,某个临时变量被赋予某个函数的返回值。一般来说,这个变量不会有什么影响,但是如果这个变量妨碍了其他重构手法,你就应该将之内联化;

2.做法

A.检查给临时变量赋值的语句,确保等号后边的赋值语句没有副作用;

B.如果这个临时变量没有被声明为final,那就声明为final,然后编译(可以检查该临时变量是否只被赋值一次);

C.找到该临时变量的所有引用点,替换成“为临时变量赋值的语句”表达式;

D.每次修改后,编译并测试;

E.修改完所有引用点后 ,删除该临时变量的声明和赋值语句;

F.编译、测试;

四.Replace Temp with Query(以查询取代临时变量)

程序以临时变量保存某一表达式的运算结果,将这个表达式提炼到独立函数中,把表达式的所有调用点换成独立函数的调用。

private int getResult(){
int result = num * price;
if (result > 10){
return result + 20;
}
return result + 10;
}


private int getResult(){
if (getCaculateResult() > 10){
return getCaculateResult() + 20;
}
return getCaculateResult() + 10;
}

private int getCaculateResult(){
return num * price;
}


1.动机

A.临时变量的问题在于作用域只在函数内,如果访问需要的临时变量,可能会驱使写出很长的代码;如果把临时变量换成查询,那么整个类中的所有函数都可以访问;

2.做法

A.找出只被赋值一次的临时变量(如果某个临时变量被多次赋值,考虑采用Split Temporary Variable,将之分割成多个临时变量

B.将该临时变量声明为final

C.编译(可检测该临时变量是否只被赋值一次)

D.将“对该变量赋值”的语句等号右侧提炼到独立函数中(1.首先将函数声明为private,以后如有需求再放开权限;2.确保提炼出来的函数无任何副作用,如果有的话,对它进行Separate Query from Modifier)

E.编译、测试

F.对该临时变量使用Inline Temp

3.范例

private double getPrice(){
int basePrice = quantity * itemPrice;
double discountFactor;
if (basePrice > 100){
discountFactor = 0.95;
}else {
discountFactor = 0.98;
}
return basePrice * discountFactor;
}


我希望将两个临时变量全都替换掉,当然每次一个。首先将变量声明为final,确认临时变量只被赋值一次(如果不是的话那说明当前的重构是不可用的)

private double getPrice(){
final int basePrice = quantity * itemPrice;
final double discountFactor;
if (basePrice > 100){
discountFactor = 0.95;
}else {
discountFactor = 0.98;
}
return basePrice * discountFactor;
}

 接下来替换临时变量,将等式右侧的表达式提炼出来;

private double getPrice(){
final int basePrice = basePrice();
final double discountFactor;
if (basePrice > 100){
discountFactor = 0.95;
}else {
discountFactor = 0.98;
}
return basePrice * discountFactor;
}

private int basePrice(){
return quantity * itemPrice;
}

编译并测试,没问题的话将临时变量调用点全部换成目标函数

private double getPrice(){
final double discountFactor;
if (basePrice() > 100){
discountFactor = 0.95;
}else {
discountFactor = 0.98;
}
return basePrice() * discountFactor;
}


然后以类似办法处理discountFactor

private double getDiscountFactor(){
if (basePrice() > 100){
return  0.95;
}else {
return  0.98;
}
}

最后得到函数:

private double getPrice(){
return basePrice() * getDiscountFactor();
}


五.Introduce Explaining Variable(引入解释性变量)

你有一个复杂的表达式,将该表达式(或其中的一部分)的结果放进一个临时变量,以此变量名称来解释表达式用途。

if ((browser.toUpperCase().indexOf("MAC") == -1) && (platform.toUpperCase().indexOf("IE") == -1) && isWaitInitialized() && resize > 10) {
// do something
}

改写成:

boolean isMacOS = (browser.toUpperCase().indexOf("MAC") == -1);
boolean isIEBrowsers = (platform.toUpperCase().indexOf("IE") == -1);
boolean isResized = (resize > 10);
if ( isMacOS && isIEBrowsers  && isWaitInitialized() && isResized) {
// do something
}


1.动机

A.表达式可能非常难读和理解,换成临时变量可以有助于分解表达式;尤其是在条件逻辑中,以一个良好命名的临时变量来解释对应条件字句的意义。

B.Introduce Explaining Variable和Replace Temp with Query有异曲同工之妙。差别在于临时变量限制较大,只能在函数内部使用;而独立函数可以在整个类中使用。但是当Extract Method有困难的时候,使用Introduce Explaining Variable还是很方便的。

2.做法

A.声明一个final的临时变量,将待分解之复杂表达式或部分运算结果赋值给它;

B.将表达式中的“运算结果”替换成上述临时变量;

C.编译、测试;

六.Split Temporary Variable(分解临时变量)

程序中有某个临时变量被赋值超过一次,它既不是循环变量,也不用于收集计算结果,针对每次赋值,创建一个独立的、对应的临时变量。

float temp = 2 * (height + width);
System.out.println("temp = " + temp);
temp = height * width;
System.out.println("tem = " + temp);

修改成:

float temp = 2 * (height + width);
System.out.println("temp = " + temp);
float area = height * width;
System.out.println("area = " + area);


1.动机

A.临时变量有各种用途,某些变量自然会导致重复赋值。循环变量和结果收集变量就是两个典型例子:循环变量会随着每次循环而发生改变,结果收集变量负责将“通过整个函数运行”的结果收集起来。除了这两种情况外,还有很多临时变量用于保存一段冗余代码的计算结果,以便稍后使用。这种临时变量应该只被赋值一次。如果被赋值超过一次,就意味着在程序中它承担了多个责任,它就应该被替换成多个临时变量,每个变量只承担一个责任。同一个临时变量承担两件不同的事情,会使程序阅读者糊涂;

2.做法

A.在待分解的临时变量声明及第一次赋值处,修改其名称;

B.将新的临时变量声明为final;

C.以该临时变量的第二次赋值动作为界,修改此前对该临时变量的所有引用点,让他们信用新的临时变量;

D.在第二次赋值处,重新声明原先的那个临时变量;

E.编译、测试

F.重复上述过程,每次都在声明处修改变量名称,并修改下次赋值之前的引用点;

七.Remove Assignments To Parameters(移除对参数的赋值)

如果程序中对一个参数赋值,那么就用一个临时变量取代该参数的位置。

private void test(int count,double price){
if (count > 10){
price = price * 0.9;
}
}

改成

private void test(int count,double price){
double result = price;
if (count > 10){
result = result * 0.9;
}
}


1.动机

A.避免降低代码清晰度,如果只把参数当作被传递进来的东西会清晰很多

B.混淆了java按值传递和引用传递,前者对参数的任何修改,都不会对调用段产生影响;

2.做法

A.建立一个临时变量,把待处理的参数值赋值给它;

B.以“对此参数赋值”为界,把界限之后对参数的所有引用点替换成对此临时变量的调用;

C.编译、测试

八.Replace Method with Method Object(以函数对象替代函数)

你有一个大型函数,由于局部变量过多使你无法采用Extract Method(提炼方法),将这个函数放到一个单独对象中,如此局部变量就变成了对象内的字段,然后你就可以在同一个对象中将这个大型函数分解成多个小型函数;

1.动机

A.对付局部变量,可以采用Inline Temp和Replace Temp with Method,但是如果函数过大,有时候这两种方式都解决不了问题。此时,应该考虑用对象的方式,这样可以把所有局部变量变成函数对象的变量,然后可以对此函数使用Extract Method方法提炼成一个个小函数;

2.做法

A.建立一个新类,根据待分解函数的用途为此对象命名;

B.在新类中建立一个final字段,用以保存原大型函数所在对象,称为源对象。同时,针对原函数的每个参数和临时变量,在新类中建立一个对应字段保存;

C.在新类中建立一个构造函数,用以接受原函数所在对象及所有参数;

D.在新类中建立一个compute()函数;

E.将原函数的所有代码复制到compute中,如果需要调用源对象的任何参数,请通过源对象调用;

F.编译

G.将就函数的函数本体替换成这样一句话“创建上述新类的一个新对象,而后调用其中的compute()函数”

九.Substitute Algorithm(替换算法)

你要想把某个算法替换成另外一个算法,将函数本体替换为另外一个方法

private String findPerson(String[] persons){
for (int i = 0;i < persons.length;i++){
if(persons[i].equals("张三")){
return "张三";
}else if (persons[i].equals("李四")){
return "李四";
}else if (persons[i].equals("王五")){
return "王五";
}
}
return "";
}

替换成

private String findPerson(String[] persons){
List candidates = Arrays.asList(new String[]{"张三","李四","王五"});
for (int i = 0;i < persons.length;i++){
if(candidates.contains(persons[i])){
return persons[i];
}
}
return "";
}


1.动机

A.解决问题用多种方法,总有某些方法会比较容易,当你觉得当前的算法逻辑有更好的取代方案时,那你就替换上就好了

2.做法

A.准备好另外一个算法,并测试通过编译

B.针对现有测试,替换上述新算法,如果结果与之前一致,重构结束;如果不一致,在测试和调试过程中,以就算法为比较参考标准;


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