这是《重构-改善既有代码的设计》这本书的原文
动机
提炼函数是我最常用的重构之一。(在这儿我用了“函数/function”这个词,但换成面向对象语言中的“方法/method”,或者其他任何形式的“过程/procedure”或者“子程序/sybroutine”,也同样适用)我会浏览一段代码,理解其作用,然后将其提炼到一个独立的函数中,并以这段代码的用途为这个函数命名。 对于“何时应该把代码放进独立的函数”这个为题,我曾经听过多种不同的意见,有的观点从代码的长度考虑,认为一个函数应该能在一屏中显示。有的观点从复用的角度考虑,认为只要被用过不止一次的代码,就应该单独放进一个函数;只用过一次的代码则保持内联(inline)的状态。但我认为最合理的观点是“将意图与实现分开”:如果你需要花时间浏览一段代码才能弄清楚它到底在干什么,那么就应该将其提炼到一个函数中,并根据他所做的事为其命名,以后再读到这段代码时,你一眼就能看到函数的用途,大多数的时候根本不需要关心函数如何达成其用途(这是函数体内干的事)。
一旦接受了这个原则,我就逐渐养成一个习惯:写非常小的函数----通常只有几行的长度。在我看来,一个函数一段超过六行,就开始散发臭味。我甚至经常会写一些只有一行代码的函数。Kent beck曾向我展示最初的Smalltalk系统中的一个例子,从那时起我就接受了“函数名的长度不重要”的观念。那时运行Smalltalk的计算机只有黑白屏显示器,如果你想高亮凸显默写文本或图像,就需要翻转视频的现实。为此,Smalltalk用于控制图像显示的类有一个叫做highlight的方法,其中的实现就只是调用reverse方法。在这个例子里,highlight方法的名字比实现还长,但这并不重要,因为在这个方法中,代码的意图与实现之间有着相当大的差距。
有些人担心段函数会造成大量函数调用,因而影响性能。在我尚且年轻时,有时确实会有这个问题;但如今“由于函数调用影响性能”的情况已经非常罕见了。短函数常常能让编译器的优化功能运转更良好,因为段函数可以更容易地被缓存。所以,应该始终遵循性能优化的一般指导方针,不用过早担心性能问题。
小函数得有个好名字才行,所以你必须在命名上花心思。起好名字需要练习,不过一旦你掌握了其中的技巧,就能写出很有自描述性的代码。
我经常会看见这样的情况: 在一个大函数中,一段代码的顶上放着一句注释,说明这段代码要做什么。在把这段代码提炼到自己的函数中时,这样的注释往往会提示一个好名字。
做法
创造一个新函数,根据这个函数的意图来对它命名(以它“做什么”来命名,而不是以它“怎么做”命名)。
如果想要提炼的代码非常简单,例如只要新函数的名称能够以更好的方式昭示代码意图,我还是会提炼它。不过,我不一定非得马上想出最好的名字,有时在提炼的过程中好的名字才会出现。有时我会提炼一个函数,尝试使用它,然后发现不太合适,再把它内联回去,这完全没问题。只要在这个过程中学到了东西,我的时间就没有白费。
如果编程语言支持潜逃函数,就把新函数嵌套在源函数里,这能减少后面需要处理的超出作用于的变量个数。我可以稍后再使用《搬移函数》把它从源函数中搬移出去。 将带提炼的代码从源函数复制到新建的目标函数中。仔细检查提炼出的代码,看看其中是否引用了作用于限于源函数、在提炼出的新函数中访问不到的变量。若是,一参数形式将它们传递给新函数。
如果提炼出的新函数嵌套在源函数的内部,就不存在变量作用于的问题了。
这些“作用域限于源函数”的变量通常是局部变量或者源函数的参数。最通用的做法是将它们都作为参数传递给新函数。只要没有提炼部分对这些变量赋值,处理起来就没有什么难度。 如果某个变量是在提炼部分之外声明但只在提炼部分被使用,就把变量声明也搬移到提炼部分代码中去。
如果变量按照传递给提炼部分又在提炼部分被赋值,就必须多加小心。如果只有一个这样的变量,我会尝试将提炼出的新函数变成一个查询,用其返回值给该变量赋值。
但有时在提炼部分被赋值的局部变量太多,这是最好先放弃提炼。这种情况下,我会考虑先使用别的重构手法,例如拆分变量或者以查询取代临时变量,来简化变量的使用情况,然后再考虑提炼函数。所有变量都处理完之后,编译。如果编程语言支持编译期检查的话,在处理完所有变量之后再做一次编译是很有用的,编译器经常会帮你找到没有被恰当处理的变量。在源函数中,将被提炼代码段替换为对目标函数的调用。
测试。
查看其他代码是否有与被提炼的代码段相同或者相似之处。如果有,考虑使用以函数调用取代内联代码令其调用提炼出的新函数。有些重构工具直接支持这一步。如果工具不支持,可以快速搜索一下,看看别处是否还有重复代码。