0%

重构:改善既有代码的设计 学习笔记

重构:改善既有代码的设计 学习笔记

重构是在不改变软件可观测行为的前提下,调整代码结构,提高软件的可理解性,降低变更成本。重构除了能够帮助我们更好的进行开发之外,没有任何意义。对于每个稍微大一点的工程项目或者有追求的程序员,都应该尽可能地去重构每一段代码。

一、重构是什么以及为什么

  1. 重构是在不改变软件可观测行为的前提下,调整代码结构,提高软件的可理解性,降低变更成本。
  2. 重构是一种经济适用行为,而非道德使然,如果它不能让我们更快更好的开发,那么它是毫无意义。
  3. 代码的写法应该使别人理解它所需要的时间最小化,进而变更代码需要的时间也会最小化。
  4. 重构对个体程序员的意义是提高ROI。
  5. 更快速的定位问题,节省调试时间。
  6. 最小化变更风险,提高代码质量,减少修复事故的时间。
  7. 得到程序员同行的认可,更好的发展机会。
  8. 重构对整个研发团队的意义是战斗力的提升。

重构与不重构

尽管重构会让我们的开发变慢,但是可以让我们将来的开发变快。如果我们要对一个项目进行长期跟进,那么重构是必不可少的。

二、重构的原则

  1. 重构的目标: 提高迭代效率,如果你确定这段代码你将来只会用到一次,且别人也不会去看你的代码,那么就没有重构的必要。
  2. 获得同行认可的方法: 每一次提交代码,都应该使代码变得更好,先重构,再开发。
  3. 增量式重构 = 自动化测试 + 持续集成 + TDD驱动重构。

三、代码的坏味道

24重代码的坏味道和例子

一般我们能接触到的一些常见的问题:

  • 命名不规范
  • 代码重复
  • 代码过长
  • 函数参数列表不易理解
  • 相关联的一些数据没有成组(数据泥团)

四、一些例子

4.1 依赖传递

变更放大:一次迭代需要修改 N 个位置,容易遗漏或失误。

关注放大:为完成修改任务,需要通读修改点上下文若干行代码,而由于依赖被传递,附近的代码会牵扯出更多需要关注的代码,往往阅读的代码量是本身要修改部分的若干倍。演化到最后就会导致不知道该次修改会不会导致问题。

4.2 神秘命名

代码/注释都是一堆符号的集合,如果这些符合不能被人或者因为其信息的冗余无效性增加了阅读负担就会降低可理解性。
好的命名应该有三种境界: 信,达,雅。
信: 准确无误地表达清楚行为的意义,做到见名之意。
达: 考虑命名对整体架构的影响,与架构的设计哲学风格统一。
雅: 生动形象,看到名字即可准确理解其在整个程序之中的作用,并能产生辅助理解的形象。

坏的例子

4.3 过度设计

当过分的考虑程序未来所要面对的需求时,将陷入过度设计的陷阱,为了未来用不上的能力,而使当下的程序变得复杂。

设计变得复杂,是因为考虑了过多的设计约束,而这些约束很可能是现在和未来都不需要的,错把这些约束条件当作了目的,而使得目标被放大,设计出没有解决实际问题的系统。

过分放大未来的某行风险,这些风险发生的概率过低,在项目可见的生命周期内都不可能遇到,因此也没必要进行设计。

4.4 结构泥团

对于核心的数据结构,没有规范化的设计将导致混乱

4.4.1 艰难引用

未充分的考虑数据结构的读取场景,导致在需要使用某些数据的时候无法简单的获得其引用,或者为了使用某个字段,需要了解一堆中间封装的数据结构。

例如:a.b.c.d.e();

4.4.2 全局盲区

大型项目的开发中,由于大家缺乏全局视角,对数据结构或者接口的设计不可避免的造成冗余或混乱,接口与结构的设计充满局部最优解,但从项目整体上看却成为一团泥球。

五、什么时候需要重构

  1. Code review: 在给别人 code review 时嗅出坏味道,在不失礼貌的前提下提出建议。
  2. 每次 commit 代码时: 每一次经你之手提交的代码都应该比之前更加干净。
  3. 当你接手一个异常难读的项目时: 说服项目组将重构作为一项需求任务来做。
  4. 当迭代效率低于预期时: 将重构当作一个项任务专门来做,必要的时候停下来迭代需求。

六、重构的基本步骤

6.1 代码分析

通读代码,分析现状,找到代码在各个层面的坏味道。

6.2 重构计划

重构应该永远是一种经济驱动的决定。

  • 对坏味道进行宣讲,并向团队给出重构的理由,以及重构的计划。
  • 确定重构的目标,明确的描述出重构后能达到的预期是什么。
  • 重构计划中必须给出测试验证方案,保证重构前与重构后软件的行为一致。
  • 如果没有这样的方案,那就必须先让软件具有可测试性。
  • 如果无法得到团队的认可,那就偷偷进行,因为重构始终是对自己有利的(减少工作量以及获得同事的认可)
  • 将重构任务当作项目来管理,对指定任务的人明确的排期和进度同步。

6.3 小步子策略

  • 将重构任务拆分成每周都能见到一点效果的小任务。
  • 每一步重构都要具有收益,并且可测试,不能阻断当前需求的迭代。
  • 重构任务必须被跟踪,要定期的开会同步进度,来不断加强团队的重构意识。

6.4 测试驱动

  • 对于小型软件,需要先补充单元测试再进行重构。
  • 对于大型软件,先搭建自动化测试流程,再进行重构。
  • 对于复杂的不确定性业务,也可以使用ab test来验证重构对指标的影响,避免造成效果/广告的损失。
  • 要保证测试的完备性与可复用性,尽可能的做到团队级的复用。
  • 保证测试环境与生产环境的一致性也是测试驱动的重要环节。

6.5 提交规范

  • 每次提交尽量控制在2分钟可以给code review的同事讲明白的程度
  • 重构应该被当作一次专门的commit中完成,在commit中写清楚改动点&测试点
  • 提交规范有助于定位bug,也是代码可读性的一个重要环节

6.6 自动化测试

  • 构建可测试的软件,首先要构建可测试的环境。
  • 对于简单应用软件可以使用单元测试,mock数据进行测试,并与ci/cd流程集成。
  • 对于复杂应用软件可以采样收集线上真实用户行为日志,mock数据周期性巡检测试。
  • 对于幂等性业务,可以mock user进行全方位的端到端自动化巡检测试。
  • 每一次功能的提交应该对应一套完整的自动化测试的策略脚本以及&监控指标与报警规则

6.7 调试BUG

  1. 亲自复现问题,关注第一现场,确定是必现还是偶现?
  2. 区分是人的问题还是环境的问题?
  3. 如果是人的问题,那是配置参数的问题还是代码逻辑的问题?
  4. 如果是配置参数的问题,则通过对比正常运行的配置参数发现问题
  5. 如果是代码逻辑的问题,则通过cimmit的历史二分查找缩小出现问题的逻辑范围
  6. 如果是机器的问题,确定是单机问题还是集群问题。
  7. 如果是单机问题,则替换机器,如果是集群问题则考虑升级硬件设备。

七、一些实际的问题

7.1 代码所有权

代码仓库的所有权会阻碍重构,调用方难以重构被调用方的代码(接口),进而导致自身重构的受阻,使得效率降低,为提高开发的效能,允许代码仓库在内部开源化,其他团队的工程师可以通过 pr 自己来实现代码,并提交给仓库的 onwer,来 code review 即可。

7.2 没有时间重构

这是重构所面临最多的借口,是自己也是团队的借口。 为此必须要明确重构是经济行为而不是一种道德行为,重构使得开发效率变得更高,因此仅对必要的代码进行重构,某个工作行为如果重复三次就可以认为未来也会存在重复,因此通过重构使得下次工作更加高效,这是一种务实的作法,而重构不一定是需要大规模的展开的任务,重构应该是不断持续进行的,将任务拆解为多个具有完备性的任务,每周完成一个,每个任务的上线都不会引起问题,并使项目变得更好,这是一种持续重构的精神态度,是高效能程序员最应该具有的工作习惯。

如果你在给项目添加新的特性,发现当前的代码不能高效的完成这个任务,并且同样的任务出现三次以上,那么这时你应该先重构,再开发新特性。

7.3 重构导致 bug

历史遗留的代码实在太多,难以阅读理解,如果无法理解谁也不敢轻易重构,害怕招致 bug 引起线上事故,因此在重构之前必须有一套相对完备的测试流程,他能给予程序员信心,也是重构的开始,反过来想对于谁也不愿意重构的代码进行重构,将收益巨大(这个项目还会继续迭代时)。