何时重构
再次说明:重构不一定要专门花整块的时间去做,它可以融入日常的工作中,编码和重构可以同时进行。那什么时候开始重构,或者说有哪些类型的重构呢?
Don Roberts给了我一条准则:第一次做某件事时只管去做;第二次做类似的事会产生反感,但无论如何还是可以去做;第三次再做类似的事,你就应该重构。 ? 正如老话说的:事不过三,三则重构。
预备性重构:让添加新功能更容易
在做新功能时,如果我们发现,对于需要做的功能,某些已经有的代码已经可以满足大部分,只需要进行简单的修改,比如添加删除参数、修改变量名,就能实现要新做的功能,那此时我们就可以对这部分代码进行重构,让它变得可以重用。
因为如果不重构,我们就需要把那段代码复制一份,修改那一小部分,当未来的某天我们要修改这个功能,那可能就需要修改两个地方,而后假使又有一个类似的功能,又需要复制一份出来。。
修复BUG时的情况也是一样,在寻找问题根因时,我可能会发现:如果把3段一模一样且都会导致错误的代码合并到一处,问题修复起来会容易得多。或者,如果把某些更新数据的逻辑与查询逻辑分开,会更容易避免造成错误的逻辑纠缠。用重构改善这些情况,在同样场合再次出现同样bug的概率也会降低。
你可能要说,这不就增加了工作量吗?并不一定,事实上大部分情况下都会减少工作量,《重构》书中举了个例子很贴切:
这就好像我要往东去100公里。我不会往东一头把车开进树林,而是先往北开20公里上高速,然后再向东开100公里。后者的速度比前者要快上3倍。如果有人催着你“赶快直接去那儿”,有时你需要说:“等等,我要先看看地图,找出最快的路径。”这就是预备性重构于我的意义。
帮助理解的重构:使代码更易懂
这里就不赘述了,上一篇文章里为何要重构里也写了,重构可以让代码更易读,所以当我们任何读代码读不懂的时候,都可以进行重构,比如更改变量的命名、整理代码的结构、太大的函数拆离成几个小函数等等,都可以让代码变得更清晰更易读。
Ralph Johnson说,这些初步的重构就像扫去窗上的尘埃,使我们得以看到窗外的风景。在研读代码时,重构会引领我获得更高层面的理解,如果只是阅读代码很难有此领悟。有些人以为这些重构只是毫无意义地把玩代码,他们没有意识到,缺少了这些细微的整理,他们就无法看到隐藏在一片混乱背后的机遇。
捡垃圾式重构
什么叫捡垃圾式重构?就是你已经可以理解代码在做什么,但发现它做的不够好,例如逻辑不必要地迂回复杂,或者两个函数几乎完全相同,可以用一个参数化的函数取而代之。这时候就可以进行重构。
这种重构是帮助理解的重构的一个变体,当我们就遇到这种情况,就需要进行一个取舍,是先戴开发新功能的帽子,还是先戴重构的帽子?这就要根据具体情况来看了,如果重构不影响你的开发进度,就可以立马进行,如果重构花费的时间比较长,或者说哪怕你的心情不好,也可以不去重构。
但是这里还是建议如果发现了可以进行重构的代码,但是没有时间进行重构,可以拿便签记下来,有空闲时再返回来完成。要相信,这些看似无意义的工作,其实是非常值得的。
当然,有时这样的垃圾需要好几个小时才能解决,而我又有更紧急的事要完成。不过即便如此,稍微花一点工夫做一点儿清理,通常都是值得的。正如野营者的老话所说:至少要让营地比你到达时更干净。如果每次经过这段代码时都把它变好一点点,积少成多,垃圾总会被处理干净。重构的妙处就在于,每个小步骤都不会破坏代码——所以,有时一块垃圾在好几个月之后才终于清理干净,但即便每次清理并不完整,代码也不会被破坏。
有计划的重构
前面的预备性重构、帮助理解的重构、捡垃圾式重构都属于见机行事类的重构:我们并不安排专门的时间段去做重构的工作,而是伴随着开发工作或者修改缺陷的工作同时进行的。那相对的,有计划的重构就是专门安排时间去优化代码。
专门花时间来重构,这会给人带来一种错觉:花这些时间来重构是为了弥补过去犯的错误,或者清理以前写的垃圾代码,其实并不是,《重构》书里再次强调了这一点,重构是为了更好的写新功能:
长久以来,人们认为编写软件是一个累加的过程:要添加新功能,我们就应该增加新代码。但优秀的程序员知道,添加新功能最快的方法往往是先修改现有的代码,使新功能容易被加入。所以,软件永远不应该被视为“完成”。每当需要新能力时,软件就应该做出相应的改变。越是在已有代码中,这样的改变就越显重要。
说这些并不是要说明有计划的重构是错的,而是说我们应该尽量多的进行“见机行事的重构”,但当现实情况导致团队过去忽视了重构,那还是需要专门花一些时间进行“有计划的重构”的。
在重构上花一个星期的时间,会在未来几个月里发挥价值。有时,即便团队做了日常的重构,还是会有问题在某个区域逐渐累积长大,最终需要专门花些时间来解决。但这种有计划的重构应该很少,大部分重构应该是不起眼的、见机行事的。
代码复审时重构
这很好理解,在code review时,我们很容易能发现别人或自己写的代码有什么不好的地方,多个人进行讨论能得到更好的结论、更好的建议。
何时不应该重构
简单来说就是不需要重构时,就不应该进行重构。(听着像废话)
听起来好像我一直在提倡重构,但确实有一些不值得重构的情况。. ? 如果我看见一块凌乱的代码,但并不需要修改它,那么我就不需要重构它。如果丑陋的代码能被隐藏在一个API之下,我就可以容忍它继续保持丑陋。只有当我需要理解其工作原理时,对其进行重构才有价值。 ? 另一种情况是,如果重写比重构还容易,就别重构了。这是个困难的决定。如果不花一点儿时间尝试,往往很难真实了解重构一块代码的难度。决定到底应该重构还是重写,需要良好的判断力与丰富的经验,我无法给出一条简单的建议。
重构的挑战(隐患)
重构是一种很有价值的技术,他有诸多的好处,同时也有一些挑战或者说隐患。
延缓新功能开发
如果读了之前的内容,就不会有这样的担忧了,因为:
重构的唯一目的就是让我们开发更快,用更少的工作量创造更大的价值。
如果遇到了一个大规模的重构,而需要添加的新功能很小,那可以先把新功能加上再进行重构,任何时候完成功能的优先级都是大于重构的优先级的。
一个专业的、优秀的程序员应该有这样的判断力:当前情况下我应该戴添加新功能的帽子,还是戴重构的帽子。我们应该权衡当前的情况做出正确的选择,以达成用最快速度来完成新功能,并使以后添加新功能也很快的目的。
很多管理者甚至程序员自己都会有这样的错觉,重构会拖慢开发速度,他们会以“保障开发速度”的名义压制重构。有些人试图用“整洁的代码”“良好的工程实践”之类道德理由来论证重构的必要性。这些都是是错误的。
重构的意义不在于把代码库打磨得闪闪发光,而是纯粹经济角度出发的考量。我们之所以重构,因为它能让我们更快——添加功能更快,修复bug更快。一定要随时记住这一点,与别人交流时也要不断强调这一点。重构应该总是由经济利益驱动。程序员、经理和客户越理解这一点,“好的设计”那条曲线就会越经常出现。
代码所有权
当我们需要跨团队协作开发时,经常会碰到我们调用别的团队的接口,或者别的团队调用我们团队的接口的情况,这时如果某个API是提供给别人的,而恰巧这个API需要重构,比如需要改函数名,那风险就产生了,我们可能没权限改别人的代码,这时候怎么办呢?
- 提供一个同样功能的API,同时标记当前API为deprecated,已过期的,过段时间让他逐渐退休;
- 弱代码所有制,最大限度的做到代码共享,大家都可以修改,开发人员按功能去区分责任区域;
分支
团队开发时对于版本控制我们经常这样做:每个团队成员在各自的分支上开发,开发完自己的功能后再合并回主分支,这样可以保证主分支的代码一直保持可用。但这样有个问题,就是如果大家在各自的分支上工作的时间太久的话,合并到主分支时就很困难,会产生很多代码冲突,解决冲突是一件很痛苦的事。
建议采取持续集成(Continuous Integration,CI)的方式进行开发。
在使用CI时,每个团队成员每天至少向主线集成一次。这个实践避免了任何分支彼此差异太大,从而极大地降低了合并的难度。不过CI也有其代价:你必须使用相关的实践以确保主线随时处于健康状态,必须学会将大功能拆分成小块,还必须使用特性开关(feature toggle,也叫特性旗标,feature flag)将尚未完成又无法拆小的功能隐藏掉。
测试
看起来重构会带来“风险太大,可能引入bug”的风险,进而加大测试人员的工作量,实际上并不会,因为我们重构时是由一点一点的小功能去改的,每次修改后都会进行自测,就算有问题,也是很小的问题,自测时就会发现并修改。
总体来说在基于“很小的修改组成大的重构”、“及时自测”的基础上,重构几乎不会发生大的事故,开发人员应自律、自知,坚守重构的原则,几乎就能避免产生新BUG,测试人员增加的工作量也会很小。
数据库
很多时候,重构往往伴随着数据库的修改,比如表结构修改,改字段名、加字段等。修改数据库可能引起很大的事故,怎么避免呢?答案是跟重构代码一样,由一点一点的小修改组成一个大的重构,确保每次小修改都没问题,持续集成、持续往生成环境发布,风险就很小了。
其他
《重构》里还讲了“重构、架构和YAGNI”、“重构与软件开发过程”、“重构与性能”、“重构起源何处”、“自动化重构”等,这里就不再赘述了,对于大多数程序员来说,前面讲的已经足够理解重构的原则,感兴趣的可以阅读原书,书中也推荐了其他重构相关的书,比如《重构手册》、《重构与模式》、《数据库重构》、《重构HTML》、《修改代码的艺术》等。
|