代码重构之道

导语:上周在乐问看到有同学提问:“在腾讯,代码重构有意义吗?”,之前在码客也看到提问:“关于代码重构有什么好的方法论吗?”,个人对代码重构非常感兴趣,在13年就开发接触代码重构的概念,学习相关理论方法,一直在坚持实践,现在基本已养成一种习惯了,所以周末系统梳理了重构原理、相关概念和操作技巧,抛砖引玉,跟大家分享交流

导语

最近看到有同学提问:“代码重构有意义吗?”,“关于代码重构有什么好的方法论吗?”,个人对代码重构非常感兴趣,在13年就开发接触代码重构的概念,学习相关理论方法,一直在坚持实践,现在基本已养成一种习惯了,所以周末系统梳理了重构原理、相关概念和操作技巧,抛砖引玉,跟大家分享交流

什么是重构?

Refactoring是对软件内部结构的一种调整,目的是在不改变外部行为的前提下,提高其可理解性降低其修改成本

为什么重构?

1.改进软件的设计

《重构》里有一段话非常有启发性:“一开始我所做的重构都像这样停留在细枝末节上。随着代码渐趋简洁,我发现自己可以看到一些以前看不到的设计层面的东西。如果不对代码做这些修改,也许我永远看不见它们,因为我的聪明才智不足以在脑子里把这一切都想象出来。Ralph Johnson把这种‘早期重构’描述成‘擦掉窗户上的污垢,使你看得更远’。研究代码时我发现,重构把我带到更高的理解层次上。如果没有重构,我达不到这种层次。

  • 程序员对代码所做的为了满足短期利益代码改动,或再没有完全清楚增个架构下的改动,都很容易使代码失去它的清晰结构,偏离需求或设计。而这些改动的积累很容易使代码偏离它原先设计的初衷而变得不可立即和无法维护。
  • Refactoring则帮助重新组织代码,重新清晰的体现结构和进一步改进设计。

2.提高代码质量,更易被理解

容易理解的代码可以很容易的维护和做进一步的开发。即使对写这些代码的程序员本身,容易理解代码也可以帮助容易地做修改。程序代码也是文档。而代码首先是写给人看的,然后才是给计算机看的。

3.Refactoring帮助尽早的发现错(Bugs)

  • Refactoring是一个code review和反馈的过程。在另一个时段重新审视自己或别人代码,可以更容易的发现问题和加深对代码的理解。
  • Refactoring是一个良好的软件开发习惯。

4.Refactoring可以提高开发速度

  • Refactoring对设计和代码的改进,都可以有效的提高开发速度。好的设计和代码质量实体提高开发速度的关键。在一个有缺陷的设计和混乱代码基础上的开发,即使表面上进度较快,但本质是试延后对设计缺陷的发现和对错误的修改,也就是延后了开发风险,最终要在开发的后期付出更多的时间和代价。
  • 项目的维护成本远高于开发成本

何时重构?

  1. 添加新功能时一并重构
    • 为了增加一个新的功能,程序员需要首先读懂现有的代码。
    • 重复性工作,既有的代码无法帮助你轻松添加新特性时
  2. 修补错误时一并重构
    • 为了修复一个Bug,程序员需要读懂现有的代码。
    • 修补bug时,排查逻辑困难
  3. Code Review时一并重构
    • code review 可以让他人来复审代码检查是否具备可读性,可理解性
  4. 当闻到代码的“坏味道”,便可以开始重构

何时不该重构?

  1. 代码太混乱,设计完全错误。与其Refactor,不如重写。
  2. 明天是DeadLine
    • 永远不要做Last-Minute-Change。推迟Refactoring,但不可以忽略,即使进入Production的代码都正确的运行。
    • Refactoring的工作量显著的影响最后期限。一个Task的计划是3天,如果为了Refactoring,需要更多的时间( 2天或更多)。推迟Refactoring,同步可以忽略。可以把这个Refactoring作为一个新的Task,或者安排在Refactoring的Iteration中完成。

重构原则

两顶帽子[重构]与[添加新功能]

  • 添加新功能时,你不应该修改既有代码,只管添加新功能。
  • 重构时你就不能再添加功能,只管改进程序结构。
  • 两顶“帽子”可同时进行,一会重构,一会添加新功能。

重构与设计

重构可以从很大程度上去辅助设计,通常情况下我们的设计不是能贯穿我们软件开发全过程的,在这个过程中,我们的需求变更的可能性非常大,当需求变了,设计可能也需要变,但是我们已有的实现怎么办?全部推翻也不太现实,这时候就要依靠重构来解决这种矛盾。

重构与性能

关于重构,有一个常被提出的问题:它对程序的性能将造成怎样的影响?重构并不意味着性能更好,但是重构可以让性能优化更容易!

  • 首先,一个构造良好的代码让你有足够的时间进行性能调整,因为它你可以更快速地添加功能,也就有更多时间用在性能问题上(准确的度量则保证你把这些时间用在恰当的地点)。

  • 其次,面对构造良好的程序,你在进行性能分析时便有较细的粒度,于是度量工具把你代入范围较小的程序段落中,而性能的调整也比较容易些。由于代码更加清晰,因此你能更好地理解自己的选择,更清楚哪种调整起关键作用。

重构忌讳

重构经过了十几年的发展和应用,可以说它是极限编程中程序员最爱的实践之一了,但实施过程中可能走偏,最终事与愿违

1.不懂重构,为了重构而重构

不清楚重构目标,不清楚重构范围等,浪费时间在简单的个人偏好上进行代码修改,要搞清楚什么是重构,用它来解决什么问题?

2.不知道什么时候算重构完成

重构是整理代码保持轻装前行的重要手段,然而我们也需要能够明确知道重构要做什么,最终的产出如何验证。在重构开始前可以指定具体的衡量指标,比如消除多少行重复代码、代码复杂度降低多少,代码耦合度降低多少等,有清晰的目标就知道什么时候算重构完成

3.暴力重构,不讲究方法

很多人都认同这一观点。但对遗留的应用软件、构筑过半的项目却容不得推倒重来。以我观察,在开始重构时仅凭自己对代码的理解就进行剪切、复制、删除、添加等大刀阔斧修改的人不在少数,尤其还没有完全掌握重构手法的新同学。结果当然错误百出,导致Bug一堆。修复这些错误代码少则几个小时,多则几天,这不是重构,这是重写。
重构是一种经千锤百炼形成的有条不紊的程序整理方法。在《重构》一书中Martin明确提出了68个代码级别的重构手法,这些手法都是等价的。在重构的过程中即使错了也没关系,都可以安全回退,重新开始。其中比较常用的手法就是桥接,如当我们要删除一个方法的时候,会新添加一个方法,然后将它的引用逐一的迁移过去,直到旧方法成为孤岛,就可以将它删除了。它能保证重构前与重构后的程序代码功能完全一致,从而实现安全重构。
下文会介绍重构技巧

4.没有策略,追求完美主义

重构过程中,经常出现为了消除一个坏味道,改了A类的方法,又改了B类的变量,不得不改了C类;最后发现这三者之间还有依赖,导致进行不下去了,波及面越来越广,时间越来越长,PM还在催,最后不得不放弃所有的代码。
调整一个正在运行中的系统也如治国,不要期望一次性调整到漂亮的代码或架构,而是要遵循“小步前进”的方法。从问题着手,每次重构一小步。针对一个问题有目的修改,修改完后测试,测试通过后提交代码,再进入下一轮重构。如果在改动过程中发现了其他需要修改的地方,不要顺便重构,你可以把它记下来,作为下一轮重构的内容。

这种做法在代码和模块层面都是相对比较容易实践,而针对架构层次的调整就相对比较复杂。这也是很多架构师需要去思考的问题,如何渐进式重构。不要搞一下子半年一年的重构,而是以周以月为单位,快速的迭代,能够很快的验证结果获得收益。

5.不知道结果对不对

对于简单的代码级别重构如果做得好是可以不用验证结果的,然对于模块级别或架构级别的重构,是需要的。否则会越做越不知道改的对不对,最终可能重构失败。

这个时候一个可以衡量重构的指标就体现它的价值:能时刻检验我们的成果,确认我们的重构还在解决当初的问题。目前常见的量化指标有如下四类,可供参考。

  • 数量: 代码的行数
  • 质量: 代码复杂度,耦合度,可读性,架构依赖复杂度等
  • 成本: 花费的时间
  • 回报(成果): 支持后续功能的快速叠加,解决现有因代码设计问题无法优化的矛盾等

6.只谈招式,不谈心法

《重构》是Martin和Kent对他们多年以来整理代码的实践的总结,然这背后体现的是他们对软件技术的深层次思考和经验。很多新人执着于学习重构手法而疏于学习背后的心法,有些可惜。

Robert C Martin的《代码整洁之道》和《敏捷软件开发:原则、模式与实践》、《设计模式》、Eric的《领域驱动设计:软件核心复杂性应对之道》、《架构之美》等都是帮助大家修炼心法的不错选择,他们可以让你更深层的了解代码,更高层面看待系统,锻炼你的嗅觉,提升你的代码能力。

7.不了解上下文,不与团队沟通

我们不得不承认对代码的重构是有风险的,尤其是模块或架构级别。这段代码的业务是什么,为什么当时这么设计,测试覆盖率是多少,如果这样改会不会影响到其他模块?对其他角色有什么影响?这些问题都要逐一回答。在风险相对较大的改动更要如此,需要和团队成员,各个角色进行沟通,谈论这次重构的好处和风险,获得足够的评估,从而能够做出合适的重构决策,将风险降到最低。

重构的衡量指标

上文也有提到
数量: 代码的行数
质量: 代码复杂度,耦合度,可读性,架构依赖复杂度等
成本: 花费的时间
回报(成果): 支持后续功能的快速叠加,解决现有因代码设计问题无法优化的矛盾等

代码坏味道

要进行代码重构,我们需要能识别出代码的坏味道,在《重构》一书中,作者列出了20+代码坏味道,大多数都非常认同,包括:重复代码、过长方法、条件逻辑过度复杂、分支语句等,尤其重复代码可以说是万恶之源,很多Bug都是这样来的。
构的理由

  1. 代码重复。
  2. 子程序太长。
  3. 循环太长或者嵌套太深
  4. 类的内聚性太差。
  5. 类的接口的抽象层次不一致。
  6. 参数表中参数太多。
  7. 类的内部修改往往局限于某个部分。
  8. 需要对多个类进行并行修改。
  9. 对继承体系的并行修改。
  10. 需要对多个case语句进行并行修改
  11. 相关的数据项只是被放在一起,没有组织到类中。
  12. 成员函数更多地使用了其他类的功能,而非自身类的.
  13. 过于依赖基本数据类型。
  14. 一个类不做什么事。
  15. 一连串传递流浪数据的子程序。
  16. 中间人对象什么也不干。
  17. 某个类同其他类关系过于密切。
  18. 子程序的命名太差。
  19. 数据成员被设置为公用。
  20. 派生类仅仅使用了基类的一小部分成员函数。
  21. 用注释来掩饰拙劣的代码。
  22. 使用了全局变量。
  23. 在子程序调用前使用设置代码,调用后使用收尾码。
  24. 程序包含的某些代码似乎在将来某个时候才会被用到。
    代码坏味道.png

重构技巧

简要总结下各类重构技巧,主要分为以下几个方面:

数据级的重构

  1. 用具名常量来代替魔法数。
  2. 更明确或更具信息量的名字来重命名变量。
  3. 将表达式内联化。
  4. 用函数来代替表达式。
  5. 引入中间变量。
  6. 将多用途变量转换为多个单一用途变量。
  7. 使用局部变量实现局部用途而不是使用参数。
  8. 将基础数据类型转化为类。
  9. 将一组类型码转化为类或是枚举类型。
  10. 将数组转化为对象。
  11. 封装群集。
  12. 用数据类替代传统记录。

语句级的重构

  1. 分解布尔表达式。
  2. 将复杂的的布尔表达式转换为命名精确的布尔函数。
  3. 将条件语句中不同部分中的重复代码合并。
  4. 使用break或retum而不是循环控制变量
  5. 嵌套的if-then-else语句中一旦知道结果就立刻退出,而不是仅仅赋一个返回值.
  6. 用多态来代替条件语句(尤其是重复的case语句)。
  7. 建并使用空对象,代替对空值的检测。

子程序级的重构

  1. 提取子程序。
  2. 将子程序代码内联化。
  3. 将冗长的子程序转化为类。
  4. 用简单的算法替代复杂算法。
  5. 增加参数/减少参数。
  6. 将查询操作同修改操作分开来。
  7. 合并功能相似的子程序,并用参数来区分他们。
  8. 通过传递不同的参数,让子程序体现不同的功能。
  9. 传递整个对而非特定成员。
  10. 传递特定成员而非整个对象
  11. 封装向下转型操作

类的重构

  1. 将值对象改为引用对象。
  2. 将引用对象改为值对象。
  3. 数据初始化来代替虚函数。
  4. 改变成员函数或数据的位置。
  5. 特定代码提出生成派生类。
  6. 将相似的代码合并起来放到基类中。

类接口的重构

  1. 将某成员子程序放到另一个类中。
  2. 将一个类转化成两个。
  3. 删除某个类。
  4. 隐藏委托关系。
  5. 去掉中间人。
  6. 用委托代替缗继承。
  7. 继承代替委托。
  8. 引入外部子程序。
  9. 引入扩展类。
  10. 暴露在外的成城员变量。
  11. 对不能修改的成员去掉Set 函数。
  12. 隐藏在类的外部不会使用的成员函数。
  13. 封装不会用到的成员函数。
  14. 如果基类和派生类的代码实现相似,将二者合并。

系统级的重构

  1. 为无法控制的数据创建明确的索引源。
  2. 将单向类联系改为双向类联系。
  3. 将双向的类联系改为单向类联系。
  4. 使用工厂函数而非简单的构造函数。
  5. 用异常代替错误代码,或者反其道而行之。

这些技巧有些很好理解,有些比较晦涩,后续计划将常用的技巧补充一篇Code Demo,加深理解。

基本技巧

  • 小步前进,频繁测试(保证每次重构后的测试都能正常跑通),最好有单元测试
  • 借助智能开发工具(比如VSCode右键可以将过长的函数代码拆解函数化,Android Studio等)

结语

重构应当作为开发同学的必备技能,优秀的程序员应当尽量避免低质量的代码,最好能够把重构作为日常开发的一部分,一边开发一边重构。在快速堆叠代码实现基本需求功能的基础上,写好测试用例,保证功能不变,逐步重构。也不是所有场景的业务都需要重构,比如时效只有几天的活动页可以不用重构,但核心业务是需要长期坚持重构投入的,并且投入产出比会很高。

参考资料

  • https://insights.thoughtworks.cn/refactoring/
  • https://zhuanlan.zhihu.com/p/32931437
  • https://www.cnblogs.com/levenyes/p/9903156.html
  • https://www.jianshu.com/p/f667ea3e4a11

参考的书籍和知识点如下:
代码坏味道.png

Top