《重构 改善既有代码的设计》 读书笔记(十八)

tech2022-08-19  130

第六章 重新组织函数

在重构过程中,最常遇到(或者最容易察觉)的坏味道是过长函数(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)。

最后,我们对函数的组织也就告一段落了,再往后的重构,就该交给第七章去处理了。

6.1 提炼函数(Extract Method)

当你有一段代码可以被组织在一起时,就把它独立出来吧。

将这段代码放进一个独立函数中,并用函数名称解释该函数的用途。

动机

将过长函数拆分成零件,更好命名,更好理解,可以省去大量注释。

每个函数粒度很小的时候,被复用的机会就会很大。

怎么起名:对于函数的命名,相信不少人都和我一样有着命名恐惧症。有人告诉说,要见名知意,然而,英文学渣的我。

做法

创造一个新函数,根据这个函数的意图对它命名(以它“做什么”来命名,而不是以它“怎么做”命名)。

如果提炼后的代码更不易理解——打扰了,请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):创造一个新类,将所需参数和临时变量都作为字段,新类中建立一个构造函数用以接收这些值。然后,在类里面写一个方法出来,这个方法可以取代原有的代码段部分。

最新回复(0)