在重构过程中,最常遇到(或者最容易察觉)的坏味道是过长函数(3.2节),其次是重复代码(3.1节)。
为了对付这两种,最有效的就是对其结构进行提取、整理、精练。
率先迎面向我们走来的是提炼函数(6.1 Extract Method),负责将长函数分割成小块函数。
当我们多次提炼后,发现有一部分函数体积不大,功能不多。此时我们就会说,“错了,这个方法没有必要存在。”
此时可以使用内联函数(6.2 Inline Method)重构,把一些小函数合并起来。
在提炼函数过程中,一个困难是处理局部变量。之前我们提到过,处理局部变量,要么就是把它当作参数,要么就是把它变成方法。
当作参数有个坏处,那就是容易出现过长的参数列(3.4)这种坏味道,为了处理这种坏味道,可以降低耦合——以查询取代临时变量(6.4 Replace Temp with Query)。
如果某个临时变量被赋值了多次,那么以赋值语句作为分割线,分解临时变量(6.6 Split Temporary Variable)是个不错的选择。
以函数对象取代函数(6.8 Replace Method with Method Object)能够分解哪怕最混乱的函数,代价是引入了一个新的类。
我们不提倡在方法内部给参数赋值,如果那样做了,就请移除对参数的赋值(6.7 Remove Assignments to Parameters)。
在处理完函数后,如果发现此处算法有改进的空间,那么可以替换算法(6.9 Substitute Algorithm)。
最后,我们对函数的组织也就告一段落了,再往后的重构,就该交给第七章去处理了。
当你有一段代码可以被组织在一起时,就把它独立出来吧。
将这段代码放进一个独立函数中,并用函数名称解释该函数的用途。
动机
将过长函数拆分成零件,更好命名,更好理解,可以省去大量注释。
每个函数粒度很小的时候,被复用的机会就会很大。
怎么起名:对于函数的命名,相信不少人都和我一样有着命名恐惧症。有人告诉说,要见名知意,然而,英文学渣的我。
做法
创造一个新函数,根据这个函数的意图对它命名(以它“做什么”来命名,而不是以它“怎么做”命名)。如果提炼后的代码更不易理解——打扰了,请ctrl+z撤销回去
将提炼出的代码从源函数复制到新建的目标函数中。复制的好处在于,明修栈道,暗度陈仓,待提炼函数完成之际,便可直接覆盖,在替换前后,代码均可正常运行。
仔细检查提炼出来的部分,看看其中是否引用了“作用域仅限于源函数”的变量。(一般来说如果有的话,会有编译报错,但是为了防止全局变量和临时变量同名引发的‘误会’,还是检查一下比较保险)
检查是否有“仅用于被提炼代码段”的临时变量。如果有,就直接在提炼出来的方法中写这个变量的声明。
检查被提炼代码段,看看是否有任何局部变量的值被它改变。
如果一个临时变量值被修改了,就尝试着能不能将临时变量被修改的动作提炼成一个查询。 这一步可能会用到分解临时变量(6.6 Split Temporary Variable)和以查询取代临时变量(6.4 Replace Temp with Query),来把临时变量消灭。
将某些个变量当作参数传给提炼出的函数。
编译器检查是否报错。
在源函数中,将被提炼代码段替换。
编译、测试。
范例——最简单的,没有变量的提炼函数
public void printOwing(double amount) { // print banner System.out.println("-------------"); System.out.println("-hello world-"); System.out.println("-------------"); } >>> public void printOwing(double amount) { this.printBanner(); } private void printBanner() { // print banner System.out.println("-------------"); System.out.println("-hello world-"); System.out.println("-------------"); }soEasy~
下面就是一个比较复杂的范例了。
范例——有局部变量的情况
当存在局部变量时,由于局部变量的作用域仅限于源函数,所以如果要想提炼函数,就需要花费额外功夫去处理,在有些时候,这些变量也许会导致无法进行此项重构。(重构不要强求,如果重构之后比重构前的代码更乱,不如不重构)
局部变量最简单的情况是,提炼函数中只读取,不修改,此时直接把它作为参数传给目标函数即可。
//这只是一个例子,这个的确没有实际意义 public void printOwing() { Vector<Order> orders = getOrders(); int total = 0; Iterator<Order> iter = orders.iterator(); while (iter.hasNext()) { total += iter.next().getAmount(); } System.out.println("---"); System.out.println(total); System.out.println("---"); }我们提炼代码中的最后三行时,传入一个total:
public void printOwing() { Vector<Order> orders = getOrders(); int total = 0; Iterator<Order> iter = orders.iterator(); while (iter.hasNext()) { total += iter.next().getAmount(); } this.printTotal(total); } private void printTotal(int total) { System.out.println("---"); System.out.println(total); System.out.println("---"); }这种情况下,由于变量仅仅是被读取,所以处理起来只需要作为参数即可。
范例——对局部变量再赋值
如果说,在提炼函数中,还对局部变量有操作,问题立马变得复杂,就像是我们代码中的total变量以及循环部分。
如果你发现源函数的参数被赋值,应该马上使用移除对参数的赋值(6.7 Remove Assignments to Parameters)。
移除对参数的赋值(6.7 Remove Assignments to Parameters):简单说,如果你将上文中printTotal(int total)方法内部写一句total=10;,在源函数中的total并不会发生改变。 尽可能不这样写,而是重新定义一个局部变量int total2=10;
如果被赋值的临时变量只会在提炼函数内部使用,那么这个临时变量的声明就可以直接拿到函数内部去。
如果被赋值的临时变量再源函数和提炼函数里都有使用,那么需要考虑:如果这个变量被提炼代码段后未再被使用,那么只需在提炼函数内部修改它即可;如果在被提炼代码段之后还使用这个变量,就需要在提炼函数中加一个返回值。
下面我们要接着上一个范例的例子继续提炼:
提炼前:
public void printOwing() { Vector<Order> orders = getOrders(); int total = 0; Iterator<Order> iter = orders.iterator(); while (iter.hasNext()) { total += iter.next().getAmount(); } this.printTotal(total); } private void printTotal(int total) {...}提炼后:
public void printOwing() { int total = getTotal(); this.printTotal(total); } private int getTotal() { Vector<Order> orders = getOrders(); int total = 0; Iterator<Order> iter = orders.iterator(); while (iter.hasNext()) { total += iter.next().getAmount(); } return total; } private void printTotal(int total) {...}很显然我把一大串代码都放进来了。orders只用在了被提炼代码段中,所以直接移入;由于在提炼代码前后都有total,所以我给了一个返回值。
如果说,此时觉得名字不合适,可以修改。(原书中是修改了的,只是我这里的例子和书中不太一样,就不修改了)
这样有一个问题:当返回的变量不止一个时,这样做就没有办法了。
有几种选择。最好的选择通常是:挑选另一块代码来提炼。
当临时变量过多时,提炼会变得困难,此时可以尝试先运用以查询取代临时变量(6.4 Replace Temp with Query),如果这么做了还是很难提炼,可以动用以函数对象取代函数(6.8 Replace Method with Method Object)。
以函数对象取代函数(6.8 Replace Method with Method Object):创造一个新类,将所需参数和临时变量都作为字段,新类中建立一个构造函数用以接收这些值。然后,在类里面写一个方法出来,这个方法可以取代原有的代码段部分。