“重构”这个概念来自于smalltalk圈子,也一直是优秀软件开发人员的必备技能,《重构》是 Martin Fowler 送给软件开发人员的一份礼物。

然而,重构一方面被践行者们视作破敌之策,一方面众多开发者们对重构又有着常见的谬误。本文将从软件开发者角度,再一次帮助大家定义重构。
1 对重构的“傲慢与偏见”
在开始介绍重构之前,我想给大家讲一个真实的项目经历。
从前,有位咨询顾问造访客户调研其开发项目。该系统的核心是一个类继承体系 ,顾问看了开发人员所写的一些代码。他发现整个体系相当凌乱,上层超类对系统的工作方式做了一些假设,下层子类实现这些假设。但是这些假设并不适合所有子类,导致覆写( override )工作非常繁重。
只要在超类做点修改,就可以减少许多覆写工作。在另一些地方,超类的某些意图并未被良好理解,因此其中某些行为在子类内重复出现。还有一些地方,好几个子类做相同的事情,其实可以把它们搬到继承体系的上层去做。
这位顾问于是建议项目经理看看这些代码,把它们整理一下,但是项目经理并不热衷于此,毕竟程序看上去还可以运行,而且项目面临很大的进度压力。于是项目经理说,晚些时候再抽时间做这些整理工作。
顾问也把他的想法告诉了在这个继承体系上工作的程序员,告诉他们可能发生的事情。程序员都很敏锐,马上就看出问题的严重性。他们知道这并不全是他们的错,有时候的确需要借助外力才能发现问题。程序员立刻用了一两天的时间整理好这个继承体系,并删掉了其中一半代码,功能毫发无损。他们对此十分满意,而且发现在继承体系中加入新的类或使用系统中的其他类都更快、更容易了。
项目经理并不高兴。进度排得很紧, 有许多工作要做。系统必须在几个月之后发布,而这些程序员却白白耗费了两天时间,做的工作与未来几个月要交付的大量功能毫不相干。原先的代码运行起来还算正常。
的确,新的设计更加“纯粹”、更加"整洁”。但项目要交付给客户的,是可以有效运行的代码,不是用以取悦学究的代码。顾问接下来又建议应该在系统的其他核心部分进行这样的整理工作,这会使整个项目停顿一至两个星期。所有这些工作只是为了让代码看起来更漂亮,并不能给系统添加任何新功能。
你对这个故事有什么感想?你认为这个顾问的建议(更进一步整理程序 )是对的吗?你会遵循那句古老的工程谚语吗:“如果它还可以运行,就不要动它。”
我必须承认自己有某些偏见,因为我就是那个顾问。6个月之后这个项目宣告失败,很大的原因是代码太复杂,无法调试,也无法将性能调优到可接受的水平。
所以,借由此事,为了让更多人了解重构,我想用本篇文章告诉大家到底什么是重构。
2 重构是什么
尽管Martin Fowler提出重构已经21年了,但不少人对重构的定义还是偏向模糊的。
“重构”这个词既可以用作名词也可以用作动词。
重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
重构(动词):使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。
重构的关键在于运用大量微小且保持软件行为的步骤,一步步达成大规模的修改。每个单独的重构要么很小,要么由若干小步骤组合而成。
重构是为了让代码“更容易理解,更易于修改”,并非打通任督二脉的灵丹妙药。这可能使程序运行得更快,也可能使程序运行得更慢,对此你应该有心理准备。
另外,在开发实践中有一个很好用“432”的原则,与大家共勉:
一个类包括注释代码不要超过400行;一个纯函数最好不要超过30行;函数内循环嵌套最多2层。为何重构
重构绝对不是所谓的“银弹”,不过它的确很有价值,可以帮你始终良好地控制自己的代码。重构是一个工具,它可以用于以下几个目的:
重构改进软件的设计
当人们只为短期目的而修改代码时,他们经常没有完全理解架构的整体设计,于是代码逐渐失去了自己的结构。
代码结构的流失有累积效应,经常性的重构有助于代码维持自己该有的形态。
重构使软件更容易理解
我们应该改变一下开发节奏,让代码变得更易于理解。重构可以帮我让代码更易读。开始进行重构前,代码可以正常运行,但结构不够理想。在重构上花一点点时间,就可以让代码更好地表达自己的意图——更清晰地说出我想要做的。
重构帮助找到bug
对代码的理解,可以帮我找到bug。重构能够帮助我更有效地写出健壮的代码。
重构提高编程速度
最后,前面的一切都归结到了这一点:重构帮我更快速地开发程序。
听起来有点儿违反直觉。当我谈到重构时,人们很容易看出它能够提高质量。改善设计、提升可读性、减少bug,这些都能提高质量。但花在重构上的时间,难道不是在降低开发速度吗?
21年前,行业的陈规认为:良好的设计必须在开始编程之前完成,因为一旦开始编写代码,设计就只会逐渐腐败。
重构改变了这个图景。现在我们可以改善已有代码的设计,因此我们可以先做一个设计,然后不断改善它,哪怕程序本身的功能也在不断发生着变化。由于预先做出良好的设计非常困难,想要既体面又快速地开发功能,重构必不可少。
何时重构
把重构融入工作,如果可以,每小时重构一次也不为过。Don Roberts提到过三次法则,提醒你何时可以重构:
第一次做某件事时只管去做
第二次做类似的事会产生反感,但无论如何还是可以去做
第三次再做类似的事,你就应该重构。
正如老话说的:事不过三,三则重构
预备性重构:让添加新功能更容易
重构的最佳时机就在添加新功能之前。在动手添加新功能之前,我会看看现有的代码库,此时经常会发现:如果对代码结构做一点微调,我的工作会容易得多。
修复bug时的情况也是一样。用重构改善这些情况,在同样场合再次出现同样bug的概率也会降低。
帮助理解的重构:使代码更易懂
我需要先理解代码在做什么,然后才能着手修改。
通过重构,我就把脑子里的理解转移到了代码本身。随后我运行这个软件,看它是否正常工作,来检查这些理解是否正确。如果把对代码的理解植入代码中,这份知识会保存得更久,并且你的同事也能看到。
重构带来的帮助不仅发生在将来——常常是立竿见影。
捡垃圾式重构
当你面临一个选择——你不想从眼下正要完成的任务上跑题太多,但也不想把垃圾留在原地,给将来的修改增加麻烦。
重构的妙处就在于,每个小步骤都不会破坏代码——所以,有时一块垃圾在好几个月之后才终于清理干净,但即便每次清理并不完整,代码也不会被破坏。
有计划的重构和见机行事的重构
还有一种常见的误解认为,重构就是人们弥补过去的错误或者清理肮脏的代码。很多漂亮的代码也需要重构。
软件永远不应该被视为“完成”。每当需要新能力时,软件就应该做出相应的改变。越是在已有代码中,这样的改变就越显重要。
不过,说了这么多,并不表示有计划的重构总是错的。只是要记住:分离重构提交并不是毋庸置疑的原则,只有当你真的感到有益时,才值得这样做。
长期重构
大多数重构可以在几分钟——最多几小时——内完成。让整个团队达成共识,在未来几周时间里逐步解决这个问题,这经常是一个有效的策略。
每当有人靠近“重构区”的代码,就把它朝想要改进的方向推动一点。这个策略的好处在于,重构不会破坏代码——每次小改动之后,整个系统仍然照常工作。
复审代码时重构
一些公司会做常规的代码复审(code review),因为这种活动不仅可以改善开发状况,对于编写清晰代码也很重要。
代码复审也让更多人有机会提出有用的建议,帮助我们获得更高层次的认识。
重构还可以帮助代码复审工作得到更具体的结果。不仅获得建议,而且其中许多建议能够立刻实现。最终你将从实践中得到比以往多得多的成就感。
2008年以后,随着互联网热潮的到来,以及通信行业大规模向敏捷转型,重构(和其他敏捷实践一起)开始真正受到了重视。我们可以看到这十多年里,一些团队、一些个人真的践行了重构的思想和实践,井且收到了好的效果。
下面,我们用两个在重构应用中被问得较多的问题,通过展示其重构手法,让大家更加了解重构:
如何应对重复代码?
怎样对过长函数进行分割?
3 如何应对重复代码?
重复代码可以说是重构收效最大的手法之一,有着让总代码量大大减少,维护方便,代码条理更加清晰易读的好处。
最单纯的重复代码就是“同一个类的两个函数含有相同的表达式”。这时候你需要做的就是采用提炼函数提炼出重复的代码,然后让这两个地点都调用被提炼出来的那一段代码。
如果重复代码只是相似而不是完全相同,请首先尝试用移动语句重组代码顺序,把相似的部分放在一 起以便提炼。如果重复的代码段位于同一个超类的不同子类中,可以使用函数上移来避免在两个子类之间互相调用。
下面为大家展示具体做法:
提炼函数
●创造一个新函数,根据这个函数的意图来对它命名(以它做什么”来命名,而不是
以它"怎样做”命名)。
●将待提炼的代码从源函数复制到新建的目标函数中。
●仔细检查提炼出的代码,看看其中是否弓|用了作用域限于源函数、在提炼出的新函
数中访问不到的变量。若是,以参数的形式将它们传递给新函数。
●所有变量都处理完之后,编译。.
●在源函数中,将被提炼代码段替换为对目标函数的调用。
●测试。
●查看其他代码是否有与被提炼的代码段相同或相似之处。如果有, 考虑使用以函数调用取代内联代码令其调用提炼出的新函数。
移动语句
●确定待移动的代码片段应该被搬往何处。仔细检查待移动片段与目的地之间的语
句,看看搬移后是否会影响这些代码正常工作。如果会,则放弃这项重构。
●剪切源代码片段,粘贴到上一步选定的位置上。
●测试。
如果测试失败,那么尝试减小移动的步子:要么是减少上下移动的行数,要么是一次搬移更少的代码。
函数上移
●检查待提升函数,确定它们是完全一 致的。
●检查函数体内引用的所有函数调用和字段都能从超类中调用到。
●如果待提升函数的签名不同,使用改变函数声明将那些签名都修改为你想要在超类中使用的签名。
●在超类中新建一 个函数,将某一个待提升函数的代码复制到其中。
●执行静态检查。
●移除一个待提升的子类函数。
●测试。
●逐一移除待提升的子类函数,直到只剩下超类中的函数为止。
它的重点就在于寻找代码当中完成某项子功能的重复代码,设法将它们合而为一,程序会变得更好。
光说不练假把式,我们来看个小实例:
class BadExample { public void someMethod1(){ //code System.out.println("重复代码");/* 重复代码块 */ //code } public void someMethod2(){ //code System.out.println("重复代码");/* 重复代码块 */ //code } } /* ---------------------分割线---------------------- */ class GoodExample { public void someMethod1(){ //code someMethod3(); //code } public void someMethod2(){ //code someMethod3(); //code } public void someMethod3(){ System.out.println("重复代码");/* 重复代码块 */ } }
4 怎样对过长函数进行分割?
据我们的经验,活得最长、最好的程序,其中的函数都比较短。小函数的价值在于其间接性带来的好处一一更好的阐释力、更易于分享、更多的选择。
百分之九十九的场合里,要把函数变短,只需使用提炼函数。找到函数中适合集中在一起的部分,将它们提炼出来形成一个新函数。
如果函数内有大量的参数和临时变量,它们会对你的函数提炼形成阻碍。如果你尝试运用提炼函数,最终就会把许多参数传递给被提炼出来的新函数,导致可读性几乎没有任何提升。此时,你可以经常运用以查询取代临时变量来消除这些临时元素。引入参数对象和保持对象完整则可以将过长的参数列表变得更简洁一些。
如果你已经这么做了,仍然有太多临时变量和参数,那就应该使出我们的杀手锏——以命令取代函数。如何确定该提炼哪一段代码呢?
一个很好的技巧是:寻找注释。它们通常能指出代码用途和实现手法之间的语义距离。如果代码前方有一行注释,就是在提醒你:可以将这段代码替换成一个函数 ,而且可以在注释的基础上给这个函数命名。就算只有一行代码,如果它需要以注释来说明,那也值得将它提炼到独立函数中去。
下面为大家展示具体操作:
以查询取代变量
●检查变量在使用前是否已经完全计算完毕,检查计算它的那段代码是否每次都能得
到一样的值。
●如果变量目前不是只读的,但是可以改造成只读变量,那就先改造它。
●测试。
●将为变量赋值的代码段提炼成函数。
如果变量和函数不能使用同样的名字,那么先为函数取个临时的名字。
确保待提炼函数没有副作用。若有,先应用将查询函数和修改函数分离手法隔离副作用。
●测试。
●应用内联变量手法移除临时变量。
引入参数对象
●如果暂时还没有一个合适的数据结构,就创建一个。
●测试。
●使用改变函数声明给原来的函数新增一个参数,类型是新建的数据结构。
●测试。
●调整所有调用者,传入新数据结构的适当实例。每修改一处,执行测试。
●用新数据结构中的每项元素,逐- -取代参数列表中与之 对应的参数项,然后删除原
来的参数。测试。
保持对象完整性
●新建一个空函数,给它以期望中的参数列表(即传入完整对象作为参数)。
●在新函数体内调用旧函数,并把新的参数(即完整对象)映射到旧的参数列表(即来源于完整对象的各项数据)。
●执行静态检查。
●逐一修改旧函数的调用者,令其使用新函数,每次修改之后执行测试。
●所有调用处都修改过来之后,使用内联函数把旧函数内联到新函数体内。
●给新函数改名,从重构开始时的容易搜索的临时名字,改为使用旧函数的名字,同
时修改所有调用处。
以命令取代函数
●为想要包装的函数创建一个空的类,根据该函数的名字为其命名。
●使用搬移函数把函数移到空的类里。
●可以考虑给每个参数创建一个字段 ,并在构造函数中添加对应的参数。
同样,我们来看具体实例:
class BadExample { public void someMethod(){ //function[1] //function[2] //function[3] } } /* ---------------------分割线---------------------- */ class GoodExample { public void someMethod(){ function1(); function2(); function3(); } private void function1(){ //function[1] } private void function2(){ //function[2] } private void function3(){ //function[3] } }
如今,重构的概念早已变得愈来愈宽泛,代码可以重构,系统可以重构,组织架构也可以重构,“敏捷”与“精益”的风潮也早已风靡于各领域。对程序员个人来说,掌握重构的技能,你的工作方式就能跟上时代的要求。
5 送给软件开发者的礼物
关于如何重构、重构的原则与手法,Fowler在《重构:改善既有代码的设计》里用 11 章的篇幅细致入微地给我们讲解了如何优雅地完成重构,相当于一本讲软件行业如何重构的“字典”,值得每位程序员一读。
译者: 熊节 ,林从羽
内容简介:
本书清晰揭示了重构的过程,解释了重构的原理和最佳实践方式,并给出了何时以及何地应该开始挖掘代码以求改善。书中给出了60多个可行的重构,每个重构都介绍了一种经过验证的代码变换手法的动机和技术。本书提出的重构准则将帮助你一次一小步地修改你的代码,从而减少了开发过程中的风险。
-END-
参考资料:
《重构 改善既有代码的设计》第二版 作者: [美]Martin Fowler


还没有评论,来说两句吧...