一、重构的定义
1.1、重构名称解释
对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解下性,降低其修改成本
如重构手法:提炼函数、多态取代条件表示
1.2、重构动词解释
使用一系列重构手法,在不改变软可观察行为的前提下,调整其结构。
可能会花一两个小时进行重构(动词),其间会使用几十个不同的重构(名词)
如果有人说代码在重构的过程中有一两天的时间不可用,基本上可以确定,他做的事不是重构。
重构之后的代码所做的事,应该与重构之前大致一样。但重构之后的代码不一定与重构之前行为完全一致。
比如
- 提炼函数会改变函数调用栈,程序的性能会有所改变
- 改变函数声明和搬移函数等重构会改变模块接口。
不过就用户应该关心行为而言,不应该有任何改变。如果在重构过程中发现了任何bug,重构完成后,同样的bug应该仍然存在。
1.3、重构和性能优化比较
- 重构是为了让代码“更容易理解,更易于修改”。这可能使程序运行得更快,也可能使程序运行得更慢。
- 性能优化,只关心让程序运行得更快,最终得到的代码有可能更难理解和维护。
二、为何重构
重构是一个工具,它可以用于以下几个目的。
- 改进软件的设计
- 使软件更容易理解
- 帮助找到bug
- 提高编程速度
2.1、改进软件的设计
当人们只为短期目的而修改代码时,经常没有完全理解架构的整体设计,
- 于是代码逐渐失去了自己的结构。
- 程序员越来越难通过阅读源码来理解原来的设计。
- 代码结构的流失有累积效应。
- 越难看出代码所代表的设计意图,越难保护其设计。
- 于是设计就腐败的越快。
经常性的重构有助于代码维持自己该有的形态。
完成同样一件事,设计欠佳的程序往往需要更多代码。(重复代码的例子)
2.2、使软件更容易理解
程序设计,很大程度上就是与计算机对话。
我编写代码告诉计算机做什么事,它响应是按照我的指示精确行动。
一言以蔽之,我所做的就是填补 "我想要它做什么"和"我告诉它做什么"之间的缝隙。
编程的核心就在于"准确说出我想要的"。
但当我努力让程序运转的时候,我不会想到未来出现的那个开发者。
几个月之后可能会有另一位程序员尝试读懂我的代码并对其做一些修改,但他才是最重要的。
如果一个程序员花费一周时间来修改某段代码。如果他理解了我的代码,这个修改原本只需一小时。
所以我们应该改变一下开发节奏,让代码变得更易于理解。在进行重构前,代码可以正常运行,但结构不够理想。在重构上花一点点时间,就可以让代码更好地表达自己的意图,更清晰地说出我想要做的。
2.3、帮助找到bug
对代码的理解,可以帮忙找到bug。对代码进行重构,可以深入理解代码的所作所为,并立即把新的理解反映在代码中。
搞清楚程序结构的同时,也验证了自己所做的一些假设,于是把bug揪出来。
2.4、提高编程速度
一个功能刚开始开发进展很快,但想要添加一个新功能,需要的时间就要长的多。需要花越来越多的时间去考虑如何把新功能塞进现有的代码库,不断蹦出来的bug修复起来也越来越慢。代码库看起来就像补丁摞补丁,需要细致的考古工作才能弄明白整个系统是如何工作的。这份负担不断拖慢新增功能的速度,最后恨不得从头开始重写整个系统。
良好的设计必须在开始编程之前完成,因为一旦开始编写代码,设计就只会逐渐腐败。重构改变了这个图景。现在我们可以改善已有代码的设计,因此我们可以先做一个设计,然后不断改善它,哪怕程序本身 的功能也在不断发生着变化。由于预先做出良好的设计非常困难,想要既体面又快速地开发功能,重构必不可少。
三、何时重构
- 预备性重构:让添加新功能更容易
- 帮助理解的重构:使代码更易懂
- 捡垃圾式重构
- 有计划的重构和见机行事的重构
- 长期重构
- 复审代码时重构
3.1、预备性重构:让添加新功能更容易
重构的最佳时机就在添加新功能之前。在动手添加新功能之前,看看现有的代码库,此时经常会发现:如果对代码结构做一点微调,工作会容易得多。
也许已经有个函数提供了我需要的大部分功能,但有几个字面量的值与我的需要略有冲突。如果不做重构,我可能会把整个函数复制过来,修改这几个值, 但这就会导致重复代码——如果将来我需要做修改,就必须同时修改两处(并且还要先找到这两处)。
修复bug时的情况也是一样。用重构改善情况,在同样场合再次出现同样bug的概率也会降低。
3.2、帮助理解的重构:使代码更易懂
需要先理解代码在做什么,然后才能着手修改。这段代码可能是我写的, 也可能是别人写的。一旦需要思考“这段代码到底在做什么”,可能就是看见了一段结构糟糕的逻辑,也可能希望复用一个函数,但花费了几分钟才弄懂它到底在做什么,因为它的函数命名实在是太糟糕了。这些都是重构的机会。
3.3、捡垃圾式重构
帮助理解的重构还有一个变体:已经理解代码在做什么,但发现它做得不好。
比如
- 逻辑不必要地迂回复杂
- 两个函数几乎完全相同
- 用一个参数化的函数取而代之
3.4、有计划的重构和见机行事的重构
太理想了,不用管。
指专门计划花一些时间来优化代码库,以便更容易添加新功能。 在重构上花一个星期的时间,会在未来几个月里发挥价值。
3.5、长期重构
太理想了,不用管。同上。
3.6、复审代码时重构
我的代码也许对我自己来说很清晰,对他人则不然。这是无法避免的,因为要让开发者设身处地为那些不熟悉自己所作所为的人着想,实在太困难了。
代码复审也让更多人有机会提出有用的建议,毕竟我在一个星期之内能够想出的好点子很有限。如果能得到别人的帮助,我的生活会滋润得多,所以我总是期待更多复审。
3.6.1、复审代码的好处
-
从开发者的角度来看
-
开发工程师需要按合理的逻辑化分,分模块从原始需求,然后是方案,最后是代码实现的进行讲解。
-
这个过程需要的能力:对需求的理解(有助于合理化分功能);表达能力,怎么说才能让评审的同学听懂你传递的信息;逻辑能力,是否有明的逻辑错误;心理压力承受能力,随时会有同学进行提问;
-
作为开发者要时刻提醒自己,代码不仅是给机器读,还是要给人看的,所以代码的可读性也要考量,提交代码的信息是否写得非常清楚、合理。想想什么样的代码最想让你骂娘?哈哈… 爱美之心,人皆有之。漂亮的代码,也是我们的追求,它不仅能够完成要求的功能,而且还要整齐,有条理,易于理解。
-
分享在这次需求开发过程中运用到的高级技术或一些奇淫巧技。
-
代码被 Code Review 后,评审者也相当于参与了这次开发,相当于“备份”,当你休假或正在忙别的需求的时候,这时“备份”就能帮上你的忙了。
-
对开发者的一个要求,大家统一代码风格,方便后期的维护。不推荐使用开发工具的自动格式化,手动调整会更好些,也能避免代码冲突。
-
-
从评审者的角度来看
-
代码格式方面:大家约定俗成,避免公司的代码风格不一致,新同学在这方面不太熟悉,就有可能出问题,但这类问题比较容易解决。
-
代码可读性方面:方法不要太长;变量名、方法名要能说明它的用户和类型;不要有嵌套太多层的条件语句或循环语句;循环语句中避免调用远程服务或比较耗时的方法;如果不可避免有一些注释,一定要保证注释准确且与代码完全一致。
-
业务边界和逻辑问题:思考一下有没有漏掉任何业务边界和逻辑问题。对现有业务是否有影响等。
-
错误处理:有没有对参数验证?远程调用超时或服务不可用时,有没有默认的补救错误?数据库保存出错有哪些影响?
-
代码质量和规范:遵循公司的编程规范,如:有重复代码段,就应该提取出来公用;不要在代码里随意设置常数,所有的常数应该在顶部统一定义或有专业的类;哪些变量是私有的、哪些是公有的,等等。
-
代码架构:包的组织方式,是否和代码库的风格一致,API 的定义是否 RESTful 等等。
-
四、何时不该重构
- 如果看见一块凌乱的代码,但并不需要修改它,那么就不需要重构它。 如果丑陋的代码能被隐藏在一个API之下,就可以容忍它继续保持丑陋。只有当需要理解其工作原理时,对其进行重构才有价值。
- 如果重写比重构还容易,就别重构了。这是个困难的决定。 如果不花一点儿时间尝试,往往很难真实了解重构一块代码的难度。决定到底应该重构还是重写,需要良好的判断力与丰富的经验。
五、重构的挑战
- 延缓新功能开发
- 测试,如果仔细遵循重构手法的每个步骤,应该不会破坏任何东西,但万一犯了个错误怎么办
- 遗留代码,如果面对一个庞大而又缺乏测试的遗留系统,很难安全地重构清理它。
评论区