重构--改善既有代码的设计(一)
最近在看一本名为《重构--改善既有代码的设计》一书,感觉书中内容非常之好。在此愿意利用闲暇时间,摘抄连载部分书中经典思想,一是为了和大家分享,共同努力。其次也是留作笔记,方便自己和更多同事查阅,使我们的代码环境更完美,大家共同进步。
----------------------我是分割线----------------------------------------
所谓重构(refactoring)是这样一个过程:在不改变代码外在行为的前提下,对代码做出修改,以改进程序内部结构。重构是一种经千锤百炼形成的有条不紊的程序整理方法,可以最大限度的减少整理过程中引入错误的几率。本质上说,重构就是在代码写好之后进行他的设计。
何种代码是坏代码,是需要重构的代码,在此列出所有需要重构的代码类型,我们会从中得到一些迹象,它会指出“这里有一个可以重构的解决的问题”。从而可以培养我们自己的判断力,学会判断一个类内有多少实例变量算是太大、一个函数内有多少行代码算是太长。
1、重复代码(Duplicated Code)
坏代码行列中首当其冲的就是重复代码,如果在一个以上的地点看到相同的程序结构,那么可以肯定:设法将他们合而为一,程序会变的更好。
(1)最单纯的重复代码就是“同一个类的两个函数含有相同的表达式”。这时侯你需要做的就是采用Extract Method提炼出重复代码,然后让这两个地点都调用被提炼出来的那一段代码。
(2)另一种常见的情况就是“两个互为兄弟的子类内含有相同表达式”。要避免这种情况,只需要对两个类都使用Extract Method,然后再对被提炼出来的代码使用Pull Up Method,将它推入超类内。
(3)如果代码之间只是类似,并非完全相同,那么运用Extract Method将相似的部分和差异部分分隔开,构成单独一个函数。然后你可能发现可以运用Form Template Method或得一个Template Method设计模式。
(4)如果有些函数以不同的算法做相同的事,你可以选择其中比较清晰的一个,并使用Substitute Algorithm将其他函数的算法替换掉。
(5)如果两个毫不相关的类出现重复代码,你应该考虑对其中一个使用Extract Class,将重复代码提炼到一个独立类中,然后在另一个类内使用这个新类。但是,重复代码所在的函数也可能的确只应该属于某个类,另一个类只能调用它,抑或这个函数可能属于第三个类,而另两个类应该引用这第三个类。你必须决定这个函数放在哪最合适,并确保它被安置后就不再其他任何地方出现。
Extract Method(提炼函数)
你有一段代码可以被组织在一起并独立出来。
将这段代码放进一个独立函数中,并让函数名称解释该函数用途。
void printOwing(double amount){
printBanner();
//print details
System.out.println("name:" + _name);
System.out.println("amount:" + _amount);
}
| |
| |
\ /
\ /
void printOwing(double amount){
printBanner();
printDetails(amount);
}
void printDetails(double amount){
//print details
System.out.println("name:" + _name);
System.out.println("amount:" + _amount);
}
动机
Extract Method 是我最常用的重构手法之一。当我看见一个多长的函数或者一段需要注释才能让人理解用途的代码,我就会将这段代码放进一个独立的函数中。
有几个原因造成我喜欢简短而命名良好的函数。首先,如果每个函数的粒度都很小,那么函数被复用的机会就更大;其次,这会使高层函数读起来就像一系列注释;再次,如果函数都使细粒度,那么函数的覆写也会更容易些。
的确,如果你习惯看大型函数,恐怕需要一段时间才能适应这种新风格,而且只有当你能给小型函数很好命名时,它们才能真正的起作用,所以你需要在函数名称上下点功夫。人们有时间会问我,一个函数多长才算合适?在我看来,长度不是问题,关键在于函数名称和函数本体之间的语义距离。如果提炼可以强化代码的清晰度,那就去做,就算函数名称比提炼出来的代码还长也无所谓。
做法
创造一个新函数,根据这个函数的意图来对它命名(以它“做什么来命名”,而不是以它“怎样做”命名)
将提炼出的代码从源函数复制到新建的目标函数中
仔细检查提炼出的代码,看看其中是否引用了“作用域限于源函数”的变量(包括局部变量和源函数参数)
检查是否有“仅用于被提炼代码段”的临时变量,如果有,在目标函数中将他们声明为临时变量。
检查被提炼代码段,看看是否有任何局部变量的值被它改变。如果一个临时变量值被修改了,看看是否可以将被提炼代码段处理为一个查询,并将结果赋值给相关变量。如果很难这样做,或如果被修改的变量不止一个,你就不能仅仅将这段代码原封不动的提炼出来,你可能需要先使用Split Temporary Variable,然后再尝试提炼出来。也可以使用Replace Temp with Query将临时变量消灭掉
将被提炼代码段中需要读取的局部变量,当做参数传给目标函数。
处理完所有局部变量之后,进行编译。
在源函数中,将被提炼代码段替换为对目标函数的调用(如果你将任何临时变量移到目标函数中,请检查它们原本的声明式是否在被提炼代码的外围,如果是,现在可以删除这些声明了)
编译,测试。
范例:无局部变量
在最简单的情况下,Extract Method易如反掌,请看下面函数:
void printOwing()
{
Enumeration<E> e = _orders.elements();
double outstanding = 0.0;
//print banner
System.out.println("*****************************");
System.out.println("*****Customer Owes****");
System.out.println("*****************************");
//calculate outstanding
while(e.hasMoreElements()){
Order each = (Order) e.nextElement();
outstanding += each.getAmount();
}
//print datails
System.out.println("name" + _name);
System.out.println("amount" + outstanding);
}
我们可以轻松提炼出“打印横幅”的代码。我只需要剪切、粘贴、再插入一个函数调用动作就行了:
void printOwing()
{
Enumeration<E> e = _orders.elements();
double outstanding = 0.0;
printBanner();
//calculate outstanding
while(e.hasMoreElements()){
Order each = (Order) e.nextElement();
outstanding += each.getAmount();
}
//print datails
System.out.println("name" + _name);
System.out.println("amount" + outstanding);
}
void printBanner(){
//print banner
System.out.println("*****************************");
System.out.println("*****Customer Owes****");
System.out.println("*****************************");
)
范例:有局部变量
果真这么简单,这个重构手法的困难点在哪里?是的,就在局部变量,包括传进源函数的参数和源函数所声明的临时变量。局部变量的作用域仅限于源函数,所以当我们使用Extract Method时,必须花费额外功夫去处理这些变量。某些时候他们甚至可能妨碍我,使我根本无法进行这项重构。
局部变量最简单的情况是:被提炼代码段只是读取这些变量的值,并不修改它们。这种情况下我可以简单的将他们当做参数传给目标函数,所以如果我对下列函数:
void printOwing()
{
Enumeration<E> e = _orders.elements();
double outstanding = 0.0;
printBanner();
//calculate outstanding
while(e.hasMoreElements()){
Order each = (Order) e.nextElement();
outstanding += each.getAmount();
}
//print datails
System.out.println("name" + _name);
System.out.println("amount" + outstanding);
}
就可以将“打印详细信息”这一部分提炼为带一个参数的函数:
void printOwing()
{
Enumeration<E> e = _orders.elements();
double outstanding = 0.0;
printBanner();
//calculate outstanding
while(e.hasMoreElements()){
Order each = (Order) e.nextElement();
outstanding += each.getAmount();
}
printDetails(outstanding);
}
void printDetails(double outstanding){
//print datails
System.out.println("name" + _name);
System.out.println("amount" + outstanding);
}
必要的时候你可以使用这种手法处理多个局部变量。
如果局部变量是个对象,而被提炼代码段调用了会对该对象造成修改的函数,也可以如法炮制。你同样只需要将这个对象作为参数传递给目标函数即可。只有在被提炼代码段真的对一个局部变量赋值的情况下,你才必须采取其他措施。
范例:对局部变量再赋值
如果被提炼代码段对局部变量赋值,问题就变得复杂了。这里我们只讨论临时变量问题。如果你发现源函数的参数被赋值,应该马上使用Remove Assignments to Parameters。