在重构手法中,很大一部分是对函数进行整理,使之更恰当地包装代码。
几乎所有时刻,问题都源于Long Methods(过长函数)。
作为新手的我就是这个样子的,写代码就跟写作文一样烂,踩着一块西瓜皮,滑到哪儿就算哪儿。一个函数200行都算短的呢,很少考虑到去拆分一个函数。
因为它们往往包含了太多信息,这些信息又被函数错综复杂的逻辑掩盖,不易鉴别。
函数太长,除了逻辑之外,肯定加了很多其他次要的东西,比如数据转换,组装或者校验过程等。
对付过长的函数,一项重要的重构手法就是Extract Method,它把一段代码从原先函数中提取出来,放进一个单独函数中。
其实Extract Method很好理解,就是把过长的代码中的其中某一个逻辑独立的部分抽取出来成为一个独立的函数。
Inline Method正好相反:将一个函数调用动作替换为该函数的本体。如果在进行多次提炼之后,意识到提炼所得的某些函数并没有做任何实质事情,或如果需要回溯恢复原先函数,就需要Inline Method。
以前看点C++的时候是有内敛函数这个玩意儿的,就记得一丢丢。
Extract Method最大困难就是处理局部变量,而临时变量则是其中一个主要的困难源头。额,我想说把一个代码抽出来考虑局部变量有这么复杂么?在处理一个函数时,我喜欢运用Replace Temp with Query去掉所有可能的临时变量。如果很多地方用到了某个临时变量,我就会先运用Split Temporary Variable将它变得比较容易转换。
但有时候临时变量实在太混乱,难以替换。这个时候,我就需要使用Replace Method with Method Object。它可以让我分解哪怕最混乱的函数,代价则是引入一个新的类。额,这点倒是学习了,居然直接用一个类去重构一个函数,第一次听说过还可以这样操作。
参数带来的问题比临时变量稍微少一些,前提是你不在函数内赋值给它们。如果已经这样做了,就使用Remove Assignments to Parameters。其实这个问题就是你对传递给函数的参数在函数内部对它进行赋值了,这个是不提倡的。
函数分解完毕之后,我就可以知道如何让它工作得更好。也许我发现还有算法可以改进,从而使代码更清晰。这时我就使用Substitute Algorithm引入更清晰的算法。
接下来就是讲了各种重新组织函数的方法了,let's go!
1. 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) { System.out.println("name: " + _name); System.out.println("amount" + amount); }
动机
Extract Method是我最常用的重构手法之一。当我看见一个过长的函数或者一段需要注释才能让人理解用途的代码,我就会将这段代码放进一个独立的函数中。"需要注释才能让人理解的函数"这个值得回味一下了。
有几个原因造成我喜欢简短而命名良好的函数。
1. 如果每个函数的粒度都很小,那么函数复用的机会就越大;
2. 调用这些小函数的高层函数读起来就像一系列注释;
3. 如果函数都是细粒度的,那么函数的覆写也更容易。
说的还挺有道理的啊,我怎么没有想到这点呢?
如果你以前看惯了大型函数,那么还是需要一段时间才能适应这种风格。嗯,看到到处都是函数,一小段一小段的。然后需要
对这些小型函数很好的命名,它们才能真正起作用,所以你需要在函数命名上下点功夫。我想问用汉语拼音可以么?还有不要嫌函数名太长,只要它能使函数名称和函数本体之间的语义距离缩短到最小,即使函数名比提炼出的代码还长都无所谓。
做法
创造一个新函数,根据这个函数的意图来对它命名(以它"做什么来命名,而不是以它"怎么做"来命名)。
将提炼出的代码从源函数中复制到新建的目标函数中。
仔细检查提炼出的代码,看看其中是否引用了"作用域限于源函数"的变量(包括局部变量和源函数参数)。
检查是否有"仅用于被提炼代码段"的临时变量。如果有,在目标函数中将它们声明为临时变量。
检查被提炼代码段,看看是否有任何局部变量的值被他改变。如果一个临时变量值被修改了,看看是否可以将被提炼代码段处理为一个查询,并将结果赋值给相关变量。如果很难这样做,或如果被修改的变量不止一个,你就不能仅仅将这段代码原封不动地提炼出来。你可能需要先使用Split Temporary Variable,然后再尝试提炼。可以使用Replace Temp with Query将临时变量消灭掉。
将被提炼代码段中需要读取的局部变量,当做参数传给目标函数。
处理完所有的局部变量之后,进行编译。
在源函数中,将被提炼代码段替换为对目标函数的调用。
编译,测试
范例:无局部变量
在最简单的情况下,Extract Method易如反掌。
void printOwing() { Enumeration 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 details System.out.println("name: " + _name); System.out.println("amount: " + outstanding); }
我们可以轻松地提炼出"打印横幅"的代码,只需要剪切、粘贴、再插入一个函数调用动作就可以了。
void printOwing() { Enumeration e = _orders.elements(); double outstanding = 0.0; printBanner(); // calculate outstanding while (e.hasMoreElements()) { Order each = (Order) e.nextElement(); outstanding += each.getAmount(); } // print details 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 = _orders.elements(); double outstanding = 0.0; printBanner(); // calculate outstanding while (e.hasMoreElements()) { Order each = (Order) e.nextElement(); outstanding += each.getAmount(); } // print details System.out.println("name: " + _name); System.out.println("amount: " + outstanding); }
可以将"打印详细信息"这一部分提炼为带一个参数的函数:
// 源函数 void printOwing() { Enumeration e = _orders.elements(); double outstanding = 0.0; printBanner(); // calculate outstanding while (e.hasMoreElements()) { Order each = (Order) e.nextElement(); outstanding += each.getAmount(); } // 目标函数调用 printDetails(outstanding); } // 目标函数 void pirntDetails(double outstanding) { // print details System.out.println("name: " + _name); System.out.println("amount: " + outstanding); }
必要的话,可以用这种手法处理多个局部变量。
如果局部变量是个对象,而被提炼代码段调用了会对该对象造成修改的函数,也可以如法炮制。你同样只需将这个对象作为参数传递给目标函数即可。只有在被提炼代码真的对一个局部变量赋值的情况下,才必须采取其他措施。
范例:对局部变量再赋值
如果被提炼代码段对局部变量赋值,问题就变得复杂了。这里只讨论临时变量的问题。如果你发现源函数的参数被赋值,应该马上使用Remove Assignments to Parameters。
被赋值的临时变量也分为两种情况。
比较简单的情况是:这个变量只在被提炼代码段中使用。果真如此,你可以将这个临时变量的声明移到被提炼代码段中,然后一起提炼出去。
另一种情况是:被提炼代码段之外的代码也是用到了这个变量。这又分为两种情况:如果这个变量在被提炼代码段之后未再被使用,你只需直接在目标函数中修改它就可以了;如果被提炼代码块之后的代码还是用了这个变量,你就需要让目标函数返回变量改变之后的值。
现在来说明这几种不同的情况。
void printOwing() { Enumeration e = _orders.elements(); double outstanding = 0.0; printBanner(); // calculate outstanding while (e.hasMoreElements()) { Order each = (Order) e.nextElement(); outstanding += each.getAmount(); } printDetails(outstanding); }
现在把"计算"部分的代码块提炼出来:
void printOwing() { printBanner(); double outstanding = getOutstanding(); printDetails(outstanding); } double getOutstanding() { // calculate outstanding Enumeration e = _orders.elements(); double outstanding = 0.0; while (e.hasMoreElements()) { Order each = (Order) e.nextElement(); outstanding += each.getAmount(); } return outstanding; }
Enumeration变量e只在被提炼代码段中用到了,所有i可以将它整个搬到新函数中。double变量outstanding在被提炼代码段内外都被用到,所以必须让提炼出来的新函数返回它。
double getOutstanding() { // calculate outstanding Enumeration e = _orders.elements(); double result = 0.0; while (e.hasMoreElements()) { Order each = (Order) e.nextElement(); result += each.getAmount(); } return result; }
这里对回传值改了一个名字。
如果需要返回的变量不止一个,该怎么办?
有几种选择。最好的选择通常是:挑选另一块代码来提炼。我比较喜欢让每个函数都返回一个值,所以会安排多个函数,用以返回多个值。
临时变量往往为数众多,甚至使提炼工作举步维艰。这种情况下,我会尝试先用Replace Temp with Query减少临时变量。如果即使这么做了提炼依旧困难重重,那我就会用Replace Method with Method Object,这个重构手法不在乎代码中有多少临时变量,也不在乎你如何使用它们。
2. Inline Method(内联函数)
一个函数的本体和名称同样清楚易懂。
在函数调用点插入函数本体,然后移除该函数。
int getRating() { return (moreThanFiveLateDeliveries()) ? 2 : 1; } boolean moreThanFiveLateDeliveries() { return _numberOfLateDeliveries > 5; }
动机
有时候你会遇到某些函数,其内部代码和函数名同样清晰易读。也可能是你重构了该函数,使得其内容和名称变得同样清晰。果真如此,就应该去掉这个函数,直接使用其中的代码。间接性可能带来帮助,但非必要的间接性总让人不舒服。
另一种需要Inline Method的情况是,你手上有一群组织不甚合理的函数。你可以将它们都内联到一个大型函数中,再从中提炼出合理的小型函数。
如果使用了太多间接层,使得系统中的所有函数似乎都只是对另一个函数的简单委托,造成我们在这些委托之间晕头转向,那么通常会使用Inline Method。间接层有价值,但并不是所有的间接层都有价值。可以找到那些有用的间接层,同时去掉那些无用的间接层。
做法
检查函数,确定它不具备多态性。
如果子类继承了这个函数,就不要将此函数内联,因为子类无法覆写一个根本不存在的函数。
找出这个函数的所有被调用点。
将这个函数的所有被调用点都替换为函数本体。
编译,测试
删除该函数的定义。