【笔记】《重构:改善既有代码的设计》第6章-重新组织函数

第6章 重新组织函数

几乎所有时刻,问题都源于Long Methods(过长函数)。这很讨厌,因为它们往往包含太多信息,这些信息又被函数错综复杂的逻辑掩盖,不易鉴别。对付过长函数,一项重要的重构手法就是Extract Method,它把一段代码从原先函数中提取出来,放进一个单独函数中。Inline Method正好相反:将一个函数调用动作替换为该函数本体。如果在进行多次提炼之后,意识到提炼所得的某些函数并没有做任何实际事情,或如果需要回溯恢复原先函数,我就需要Inline Method。

Extract Method 最大的困难就是处理局部变量,而临时变量则是其中一个主要的困难源头。处理一个函数时,我喜欢运用Replace Temp with Query 去掉所有可去掉的临时变量。如果很多地方使用了某个临时变量,我就会先运用Split Temporary Variable 将它变得比较容易替换。

但有时候临时变量实在太混乱,难以替换。这时候我就需要使用Replace Method with Method Object。它让我可以分解哪怕最混乱的函数,代价则是引入一个新类。

参数带来的问题比临时变量稍微少一些,前提是你不在函数内赋值给它们。如果你已经这样做了,就得使用Remove Assignment to Parameter。

函数分解完毕后,我就可以知道如何让它工作得更好。也许我还会发现算法可以改进,从而使代码更清晰。这时我就使用Substitute Algorithm引入更清晰的算法。

6.1 Extract Method(提炼函数)

你有一段代码可以被组织在一起并独立出来。

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

动机

Extract Method 是我最常用的重构方法之一。当我看见一个过长的函数或者一段需要注释才能让人理解用途的代码,我就会将这段代码放进一个独立函数中。

有几个原因造成我喜欢简短而命名良好的函数。首先,如果每个函数的粒度都很小,那么函数被复用的机会就更大;其次,这会使高层函数读起来就像一系列注释;再次,如果函数都是细粒度,那么函数的覆写也会容易一些。

一个函数多长才算合适?在我看来,长度不是问题,关键在于函数名称和函数本体之间的语义距离。如果提炼可以强化代码的清晰度,那就去做,就算函数名称比提炼出来的代码还长也无所谓。

做法

  • 创建一个新函数,根据这个函数的意图来对它命名
  • 将提炼出的代码从源函数复制到新建的目标函数中。
  • 仔细检查提炼出的代码,看看其中是否引用了“作用域限于源函数”的变量(包括局部变量和源函数参数)
  • 检查是否有“仅用于被提炼代码段”的临时变量。如果有,在目标函数中将它们声明为临时变量。
  • 检查被提炼代码段,看看是否有任何局部变量的值被它改变。如果一个临时变量值被修改了,看看是否可以将被提炼代码段处理为一个查询,并将结果赋值给相关变量。如果很难这样做,或如果被修改的变量不止一个,你就不能仅仅将这段代码原封不动地提炼出来。你可能需要先使用Split Temporary Variable,然后再尝试提炼。也可以使用Replace Temp with Query将临时变量消灭掉。
  • 将被提炼代码段中需要读取的局部变量,当作参数传给目标函数。
  • 处理完所有局部变量之后,进行编译。
  • 在源函数中,将被提炼代码段替换为对目标函数的调用。
  • 编译,测试

范例:无局部变量

范例:有局部变量

局部变量最简单的情况是:被提炼代码段只是读取这些变量的值,并不修改它们。这种情况下我可以简单地把它们作为参数传给目标函数。

如果局部变量是个对象,而被提炼代码段调用了会对该对象造成修改的函数,也可以如法炮制。你同样只需将这个对象作为参数传递给目标函数即可。只有在被提炼代码段真的对一个局部变量赋值的情况下,你才必须采取其他措施。

范例:对局部变量再赋值

如果被提炼代码段对局部变量赋值,问题就变得复杂了。这里我们只讨论临时变量的问题。如果你发现源函数的参数被赋值,应该马上使用Remove Assignments to Parameters。

被赋值的临时变量也分为两种情况。较简单的情况是:这个变量只在被提炼的代码段中使用。果真如此,你可以将这个临时变量的声明移到被提炼代码段中,然后一起提炼出去。另一种情况是:被提炼代码段之外的代码也使用了这个变量。这又分为两种情况:如果这个变量在被提炼代码段之后未再被使用,你只需要直接在目标函数中修改它就可以了;如果被提炼代码段之后的代码还使用了这个变量,你就需要让目标函数返回该变量改变后的值。

这时候,你可能会问:“如果需要返回的变量不止一个,又该怎么办呢?”

有几种选择。最好的选择通常是:挑选另一块代码来提炼。

临时变量往往为数众多,甚至会使提炼工作举步维艰。这种情况下,我会尝试先运用Replace Temp with Query 减少临时变量。如果即使这么做了提炼依旧困难重重,我就会动用Replace Method with Method Object,这个重构手法不在乎代码中有多少临时变量,也不在乎你如何使用它们。

6.2 Inline Method(内联函数)

一个函数的本体与名称同样清楚易懂。

在函数调用点插入函数本体,然后移除该函数。

动机

有时候你会遇到某些函数,其内部代码和函数名称一样清晰易读。也可能你重构了该函数,使得其内容和其名称同样清晰。果真如此,你就应该去掉这个函数,直接使用其中的代码。

另一种需要使用Inline Method的情况是:你手上有一群组织不甚合理的函数。你可以将它们都内联到一个大型函数中,再从中提炼出组织合理的小型函数。Kent Beck发现,实施Replace Method with Method Object之前先这么做,往往可以获得不错的效果。

如果别人使用了太多间接层,使得系统中的所有函数都似乎只是对另一个函数的简单委托,造成我在这些委托动作之间晕头转向,那么我通常都会使用Inline Method。

做法

  • 检查函数,确定它不具有多态性。
  • 找出这个函数的所有被调用点。
  • 将这个函数的所有调用点都替换为函数本体。
  • 编译,测试。
  • 删除该函数的定义。

对于递归调用、多返回点、内联至另一个对象中而该对象并无提供访问函数…… 我之所以不写这些特殊情况,原因很简单:如果你遇到了这样的复杂情况,那么就不应该使用这个重构手法。

6.3 Inline Temp(内联临时变量)

你有一个临时变量,只被一个简单表达式赋值一次,而它妨碍了其他重构方法。

将所有对该变量的引用动作,替换为对它赋值的那个表达式自身。

动机

Inline Temp多半是作为Replace Temp with Query的一部分使用的,所以真正的动机出现在后者那儿。唯一单独使用Inline Temp的情况是:你发现某个临时变量被赋予某个函数调用的返回值。一般来说,这样的临时变量不会有任何危害,可以放心地把它留在那儿。但如果这个临时变量妨碍了其他的重构手法,例如Extract Method,你就应该将它内联化。

做法

  • 检查给临时变量赋值的语句,确保等号右边的表达式没有副作用。
  • 如果这个临时变量并未被声明为final,那就将它声明为final,然后编译。
  • 找到该临时变量的所有引用点,将它们替换为“为临时变量赋值”的表达式。
  • 每次修改后,编译并测试。
  • 修改完所有引用点之后,删除该临时变量的声明和赋值语句。
  • 编译,测试。

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

你的程序以一个临时变量保存某一表达式的运算结果。

将这个表达式提炼到一个独立函数中。将这个临时变量的所有引用点替换为对新函数的调用。此后,新函数就可被其他函数使用。

动机

临时变量的问题在于:它们是暂时的,而且只能在所属函数内使用。由于临时变量只在所属函数内可见,所以它们会驱使你写出更长的函数,因为只有这样你才能访问到需要的临时变量。如果把临时变量替换为一个查询,那么同一个类中的所有函数都将可以获得这份信息。

Replace Temp with Query往往是你运用Extract Method前必不可少的一个步骤。局部变量会使代码难以被提炼,所以你应该尽可能把它们替换为查询式。

这个重构手法较为简单的情况是:临时变量只被赋值一次,或者赋值给临时变量的表达式不受其他条件影响。其他情况比较棘手,但也有可能发生。你可能需要先运用Split Temporary Variable或Separate Query from Modifier使情况变得简单些,然后再替换临时变量。如果你想替换的临时变量是用来收集结果的(例如循环中的累加值),就需要将某些程序逻辑(例如循环)复制到查询函数去。

做法

首先是简单情况:

  • 找出只赋值一次的临时变量。
  • 将该临时变量声明为final。
  • 编译。
    (这可以确保该临时变量的确只被赋值一次)
  • 将“对该临时变量赋值”的语句的等号右侧部分提炼到一个独立函数中。
    (首先将函数声明为private。日后你可能会发现有更多类需要使用它,那时放松对它的保护也很容易。)
    (确保提炼出来的函数无任何副作用,也就是说该函数并不修改任何对象内容,如果它有副作用,就对它进行Separate Query from Modifier)
  • 编译,测试。
  • 在该临时变量身上实施Inline Temp。

运用此手法,你可能会担心性能问题。和其他性能问题一样,我们现在不管它,因为它十有八九根本不会造成任何影响。若是性能真的出了问题,你也可以在优化时期解决它。代码组织良好,你往往能够发现更有效的优化方案:如果没有进行重构,好的优化方案就可能与你失之交臂。如果性能实在太糟糕,要把临时变量放回去也是很容易的。

范例

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

你有一个复杂的表达式。

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

动机

表达式可能非常复杂而难以阅读。这种情况下,临时变量可以帮助你将表达式分解为比较容易管理的形式。

在条件逻辑中,Introduce Explaining Variable 特别有价值:你可以用这项重构将每个条件子句提炼出来,以一个良好命名的临时变量来解释对应条件子句的意义。使用这项重构的另一种情况是,在较长算法中,可以运用临时变量来解释每一步运算的意义。

Introduce Explaining Variable是一个很常见的重构手法,但我得承认,我并不常用它。我几乎总是尽量使用Extract Method来解释一段代码的意义。毕竟临时变量只在它所处的那个函数中才有意义,局限性较大,函数则可以在对象的整个生命中都有用,并且可被其他对象使用。但有时候,当局部变量使Extract Method难以进行时,我就使用Introduce Explaining Variable。

做法

  • 声明一个final临时变量,将待分解之复杂表达式的一部分动作的运算结果赋值给它。
  • 将表达式中的“运算结果”这一部分,替换为上述临时变量。
    (如果替换这一部分在代码中重复出现,你可以每次一个,逐一替换)
  • 编译,测试。
  • 重复上述过程,处理表达式的其他部分。

范例

运用Extract Method处理上述范例

那么,应该在什么时候使用Introduce Explaining Variable呢?答案是:在Extract Method 需要花费更大工作量时。如果我处理的是一个拥有大量局部变量的算法,那么使用Extract Method绝非易事。

6.6 Split Temporary Variable(分解临时变量)

你的程序有某个临时变量被赋值超过一次,它既不是循环变量,也不被用于收集计算结果。

针对每次赋值,创造一个独立、对应的临时变量。

动机

如果临时变量承担多个责任,它就应该被替换(分解)为多个临时变量,每个变量只承担一个责任。

做法

  • 在待分解临时变量的声明及其第一次被赋值处,修改其名称。
  • 将新的临时变量声明为final。
  • 以该临时变量的第二次赋值动作为界,修改此前对该临时变量的所有引用点,让它们引用新的临时变量。
  • 编译,测试。
  • 逐次重复上述过程。每次都在声明处对临时变量改名,并修改下次赋值前的引用点。

范例

6.7 Remove Assignment to Parameters(移除对参数的赋值)

代码对一个参数进行赋值。

以一个临时变量取代该参数的位置。

动机

在Java中,不要对参数赋值。

做法

  • 建立一个临时变量,把待处理的参数值赋予它。
  • 以“对参数的赋值”为界,将其后所有对此参数的引用点,全部替换为“对此临时变量的引用”。
  • 修改赋值语句,使其改为对新建之临时变量赋值。
  • 编译,测试。

范例

还可以为参数加上关键词final,从而强制它遵循“不对参数赋值”这一惯例。

Java的按值传递

6.8 Replace Method with Method Object(以函数对象取代函数)

你有一个大型函数,其中对局部变量的使用使你无法采用Extract Method。

将这个函数放进一个单独的对象中,如此一来局部变量就成了对象内的字段。然后你可以在同一个对象中将这个大型函数分解为多个小型函数。

动机

局部变量的存在会增加函数分解难度。

做法

  • 建立一个新类,根据待处理函数的用途,为这个类命名。
  • 在新类中建立一个final字段,用以保存原先大型函数所在的对象。我们将这个字段称为“源对象”。同时,针对原函数的每个临时变量和每个参数,在新类中建立一个对应的字段保存之。
  • 在新类中建立一个构造函数,接收源对象及原函数的所有参数作为参数。
  • 在新类中建立一个compute()函数。
  • 将原函数的代码复制到compute()函数中。如果需要调用源对象的任何函数,请通过源对象字段调用。
  • 编译。
  • 将旧函数的函数本体替换为这样一条语句:“创建上述新类的一个对象,而后调用其中的compute()函数。”

范例

6.9 Substitute Algorithm(替换算法)

你想要把某个算法替换为另一个更清晰的算法。

将函数本体替换为另一个算法。

动机

做法

  • 准备好另一个(替换用)算法,让它通过编译。
  • 针对现有测试,执行上述的新算法。如果结果与原本结果相同,重构结束。
  • 如果测试结果不同于原先,在测试和调试过程中,以旧算法为比较参照标准。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值