1、重构手法中很大一部分是对函数进行整理,使之更恰当地包装代码。几乎所有时刻,问题都源于Long Method(过长函数)。这很讨厌,因为它们往往包含太多信息,这些信息又被函数错综复杂的逻辑掩盖,不易鉴别。对付过长函数,一项重要的重构手法就是Extract Method(提炼函数),它把一段代码从原先函数中提取出来,放进一个单独函数中。Inline Method(内联函数)正好相反:将一个函数调用动作替换为该函数本体。如果在进行多次提炼之后,意识到提炼所得的某些函数并没有做任何实质事情,或如果需要回溯恢复原先函数,我们就需要Inline Method(内联函数)。
2、 Extract Method最大的困难就是处理局部变量,而临时变量则是其中一个主要的困难源头。处理一个函数时,我喜欢应用Replace Temp with Query(以查询取代临时变量)去掉所有可去掉的临时变量。如果很多地方使用了临时变量,我就会先应用Split Temporary Variable(分解临时变量)将他变得比较容易替换。
3、 但有时候临时变量实在太混乱,难以替换。这时候就需要使用Replace Method with Method Object(以函数对象取代函数)。它让我可以分解哪怕最混乱的函数,代价则是引入一个新的类。
4、 参数带来的问题比临时变量稍微少一些,前提是你不在函数内赋值给它们。如果你已经这么做了,就得使用Remove Assignments Parameters(移除对参数的赋值)。
5、 函数分解完毕后,我就可以知道如何让它工作得更好。也许我还可以发现算法可以改进,从而使代码更清晰。这时就得使用Substitute Algorithm(替换算法)引入更清晰得算法。
1、当看见一个过长的函数或者一段需要注释才能让人理解用途的代码,这时就需要考虑将这段代码放进一个独立的函数中。
2、简短且良好的函数命名。首先,如果每个函数的颗粒度都很小,那么函数被复用的机会就更大;其次这会使高层函数读起来就像一系列注释;再次,如果函数都是细粒度的,那么函数的覆写也会更容易。
❑创造一个新函数,根据这个函数的意图对它命名(以它“做什么”来命名,而不是以它“怎么做”命名)。
➾ 即使你想要提炼的代码非常简单,例如是一个消息或一个函数调用,只要新函数的名称能够以更好的方式昭示代码的意图如,你也应该提炼它。但如果你想不出一个更有意义的名称,就别动。
❑将提炼出的代码从源函数复制到新建的目标函数中。
❑仔细检查提炼出的代码,看看其中是否引用了“作用域限于源函数”的变量(包括局部变量和源函数参数)。
❑检查是否有“仅用于被提炼代码段”的临时变量。如果有,在目标函数中将它们声明为临时变量。
❑检查被提炼的代码段,看看是否有任何局部变量的值被它改变。如果一个临时变量值被修改,看看是否可以将被提炼代码段处理为一个查询,并将结果赋值给相关变量。如果很难这样做,或如果被修改的变量不止一个,你就不能仅仅将这段代码原封不动地提炼出来。你可能需要先使用Split Temporary Variable,然后再尝试提炼。也可以使用Replace Temp with Query 将临时变量消灭。
❑将被提炼代码段中需要读取地局部变量,作为参数传递给目标函数。
❑处理完所有局部变量之后,进行编译。
❑在源函数中,将被提炼代码段替换为对目标函数地调用。
➾如果你将任何临时变量移到目标函数中,请检查它们原本地声明式是否在被提炼代码段地外围,如果是,现在你可以删除这些声明式了。
❑编译,测试。
1、有时候你会遇到某些函数,其内部代码和函数名同样清晰易读。也可能你重构了该函数,使得其内容和其名称变得同样清晰。如果的确如此,那么你应该去掉这个函数,直接使用其中的代码。间接性可能带来帮助,但非必要的间接性总是让人不舒服。
❑检查函数,确定它不具多态性。
注:如果子类继承了这个函数,就不要将此函数内联,因为子类无法覆写一个根本不存在的函数。
❑找出这个函数的所有被调用点。
❑将这个函数的所有被调用点都替换为函数本体。
❑编译,测试。
❑删除该函数的定义。
1、Inline Temp 大多数时候是作为Replace Temp with Query的一部分使用的,所以真正的动机出现在后者那儿。唯一单独使用Inline Temp的情况是:当你发现某个临时变量被赋予某个函数调用的返回值。
❑检查给临时变量赋值的语句,确保等号右边的表达式没有副作用。
❑如果这个临时变量并未被声明为final,那就将它声明为final,然后编译。
➾这可以检查该临时变量是否真的只被赋值一次。
❑找到该临时变量的所有引用点,将它们替换为“为临时变量赋值”的表达式。
❑每次修改后,编译并测试。
❑修改完所有引用点之后,删除该临时变量的声明和赋值语句。
❑编译,测试。
1、临时变量的问题在于:它们是暂时的,而且只能在所属函数内使用。由于临时变量只是在所属函数内可见,所以它们会驱使你写出更长的函数,因为只有这样你才能访问到需要的临时变量。如果把临时变量替换为一个查询,那么同一个类中的所有函数都可以获得这份信息。这将带给你极大帮助,使你能够为这个类编写更清晰的代码。
2、Replace Temp with Query 往往是运用Extract Method之前必不可少的一个步骤。
❑找出只被赋值一次的临时变量。
➾如果某个临时变量被赋值超过一次,考虑使用Split Temporary Variable将它分割成多个变量。
❑将该临时变量声明为final。
❑编译。
注:这个确保该临时变量只被赋值一次。
❑将“对该临时变量赋值”之语句的等号右侧部分提炼到一个独立函数中。
注:首先将函数声明为private,日后你可能发现有更多类需要使用它,那时放松对它的保护也很容易。
确保提炼出来的函数无任何副作用,也就是说该函数并不修改任何对象内容,如果它有副作用,就对它进行Separate Query from Modifier(将查询函数和修改函数分离)。
❑编译,测试。
❑在该临时变量身上实施Inline Temp。
1、表达式有可能非常复杂而难以阅读,这种情况下,临时变量可以帮助你将表达式分解为比较容易管理的形式。 2、在逻辑条件中,Introduce Explaining Variable特别有价值:你可以用这项重构将每个条件子句提炼出来,以一个良好命名的临时变量来解释对应子句的意义。使用这项重构的另一种情况是,在较长算法中,可以运用临时变量来解释每一步算法的意义。
3、Introduce Explaining Variable是一个很常见的重构手法,但我得承认,我并不经常用它。我几乎总是尽量使用Extract Method来解释一段代码得意义。毕竟临时变量只在它所处的那个函数中才有意义,局限性较大,而函数则可以在对象的整个生命周期都有用,并且可以被其他对象使用。但有时候,当局部变量使Extract Method难以进行时,我就使用Introduce Explaining Variable。
❑声明一个final临时变量,将待分解之复杂表达式中的一部分动作的运算结果赋值给它。
❑将表达式中的“运算结果”这一部分,替换为上述临时变量。
➾如果被替换的这一部分在代码中重复出现,你可以每次一个,逐一替换。
❑编译,测试。
❑重复上述过程,处理表达式的其他部分。
1、临时变量有各种不同用途,其中某些用途会很自然地导致临时变量被赋值。“循环变量”和“结果收集变量”就是两个典型例子:循环变量(loop variable)会随循环的每次运行而改变;结果收集变量(collecting temporary variable)负责将“通过整个函数的运算”而构成的某个值收集起来。
2、除了这两种情况,还有很多临时变量用于保存一段冗长代码的运算结果,以便稍后使用。这种临时变量应该只被赋值一次。如果它们被赋值超过一次,就意味它们在函数中承担了一个以上的责任。如果临时变量承担多个责任,它就应该被替换为多个临时变量,每个变量只承担一个责任。同一个临时变量承担两件不同的事情,会令代码阅读者糊涂。
❑在待分解临时变量的声明及其第一次被赋值处,修改其名称。
❑将新的临时变量声明为final
❑以该临时变量的第二次赋值动作为界,修改此前对该临时变量的所有引用点,让它们引用新的临时变量。
❑在第二次赋值处,重新声明原先那个临时变量。
❑编译,测试。
❑逐次重复上述过程,每次都在声明处对临时变量改名,并修改下次赋值之前的引用点。
1、首先,我要确定大家都清楚“对参数赋值”这个说法的意思。如果你把一个名为foo的对象作为参数传给某个函数,那么“对参数赋值”意味改变foo,使它引用另一个对象。如果你在“被传入对象”身上进行什么操作,那没问题,我也总是这样干。
2、在按值传递的情况下,对参数的任何修改,都不会对调用端造成任何影响。那些用过按引用传递方式的人可能会在这点上犯糊涂。
❑建立一个临时变量,把待处理的参数值赋予它。
❑以“对参数的赋值”为界,将其后对所有参数的引用点,全部替换为“对此临时变量的引用”。
❑修改赋值语句,使其改为对新建之临时变量赋值。
❑编译,测试。
➾如果代码的语义是按引用传递的,请在调用端检查调用后是否还使用了这个参数,也要检查有多少个按引用传递的参数被赋值后又被使用。请尽量只以return方式返回一个值。如果需要返回的值不止一个,看看可否把需要返回的大堆数据变成单一对象,或干脆为每个返回值设计对应的一个独立函数。
1、局部变量的存在会增加函数的分解难度。如果一个函数之中局部变量泛滥成灾,那么想分解这个函数是非常困难的。这时使用Replace Method with Method Object会将所有局部变量都变成函数对象的字段。然后你就可以对这个新对象使用Extract Method创造出新的函数,从而将原本的大型函数拆解变短。
❑建立一个新类,根据待处理函数的用途,为这个类命名。
❑在新类中建立一个final字段,用以保存原先大型函数所在的对象。我们将这个字段称为“源对象”。同时,针对源函数的临时变量和每个参数,在新类中建立一个对应的字段保存之。
❑在新类中建立一个构造函数,接收源对象及源函数的所有参数作为参数。
❑在新类中建立一个compute()函数。
❑将源函数中的代码复制到compute()函数中。如果需要调用源对象的任何函数,请通过源对象字段调用。
❑编译。
❑将旧函数的函数本体替换为这样一条语句:“创建上述新类的一个新对象”,而后调用其中的compute()函数“。
1、解决问题有好几种方法,我敢打赌其中某些方法会比另一些简单。算法也是如此。如果你发现做一件事可以有更清晰的方式,就应该以比较清晰的方式取代复杂的方式。“重构”可以把一些复杂东西分解为较简单的小块,但有时候你就是必须删掉整个算法。随着对问题有了更多理解,你往往会发现,在原先的算法之外,有更简单的解决方案,此时你就需要改变原先的算法。如果你开始使用程序库,而其中提供的某些功能/特性与你自己的代码重复,那么你也需要改变原先的算法。
2、有时候你会想要修改原先的算法,让它去做一件与原先略有差异的事。这时候你也可以把原先的算法替换为一个较简易修改的算法,这样后续的修改会轻松许多。
3、使用这项重构手法之前,请先确定自己已经尽可能分解了原先函数。替换一个巨大而复杂的算法是非常困难的,只有先将它分解为较简单的小型函数,然后你才能很有把握进行算法替换工作。
❑准备好另一个(替换用)算法,让它通过编译。
❑针对现有测试,执行上述的新算法。如果结果与原本结果相同,重构结束。
❑如果测试结果不同于原先,在测试和调试过程中,以旧算法为比较参照标准。
➾对于每个测试用例,分别以新旧两种算法执行,并观察两者结果是否相同,这可以帮助你看到哪一个测试用例出现麻烦,以及出现了怎样的麻烦。