重构

208 阅读12分钟

一.重构的简要过程

  1. 构建完整的测试体系
  2. 分解复杂函数,提炼函数并检查变量作用域
  3. 小步修改,每次修改后就运行测试,防止混乱
  4. 简洁有意义的变量命名
  5. 消除临时变量,方便方法的提炼
  6. 不会更改的数据可以使用其副本
  7. 以多态取代条件表达式

二.重构的原则

  1. 重构和性能优化
  • 相同点:都需要修改代码,都不会改变程序的整体功能
  • 不同点:重构是为了让代码”更容易理解,更易于修改”,可能会让程序运行更快,也可能更慢;性能优化是为了让程序运行的更快,最终的代码可能难于理解和维护 2.两顶帽子(原则上独立,实际开发中经常变换)
  • 添加新功能
  • 重构 3.为何重构
  • 重构改进软件的设计:只是为了短期目的修改代码,经常没有完全理解架构整体设计,代码会逐渐失去原先的结构。我们经常会有需求增加一个和现有功能类似的小功能,很多人为了防止影响原先的功能,会选择复制一份原先的代码然后再更改,造成大量的重复代码,以后如果功能需要调整,就需要修改两处代码,这样的代码就不是优秀的设计。
  • 重构使软件更容易理解:代码告诉计算机做什么,清晰的代码设计可以精确的表达自己的意图。除此之外,其他程序员和自己都是这份代码的读者,代码没有经过重构优化结构时,不仅其他人理解起来比较费劲,自己在经过一段时间后再看也很难想起当时的逻辑。
  • 重构帮助找到bug:重构需要对首先深入理解代码的所作所为,并以此调整代码结构,过程中既优化了代码,也可以找出隐藏其中的bug。
  • 重构提高编程速度:很多人对重构的直觉是没有新功能的产出,仅仅浪费时间调整代码,是降低开发速度。好多系统都是刚开始进展很快,到后面想要添加一个新功能或者修复一个bug都很耗时间,每次都需要细致的考古才能理清系统的工作流程,最后恨不得重写系统。但好多团队则截然不同,代码内部设计良好,添加新功能的速度很快,可以基于现有功能快速构建新功能,出现bug也可以很轻松的调试。 1BC4B80D-FFC1-4EF8-B46B-CEB859891E5E.png 4.何时重构(事不过三,三则重构)
  • 预备性重构:让添加新功能更容易。我们在添加新功能时,一般都会看看有没有可以复用的功能,可能会涉及到对原先代码的微调。(我要向东去100公里,不会直接向东开车直行,而是先看看地图,发现可以先向北开20公里上高速,然后再向东开100公里)
  • 帮助理解的重构:使代码更易懂。我们在开发中会发现一些难理解的代码,可以对其进行重构,比如适当的变量命名,帮助我们更容易理解代码并复用。
  • 捡垃圾式重构。这是帮助理解的变体,我们已经理解了代码在做什么,但是逻辑迂回复杂,重构起来也比较容易,那就可以先记录下来,在忙完紧急工作后,再回头进行垃圾代码清理。(至少要让营地比你到达时更干净)
  • 有计划的重构和见机行事的重构。以上三种都是见机行事的重构,大部分重构都是不起眼的、见机行事的,有计划的重构会比较少,项目通常不会专门留重构的时间,如有必要也是可以的。
  • 复审代码时重构。code review可以帮助开发者优化自己的代码设计,集思广益。
  • 何时不应该重构。如果有一块凌乱的代码,但并不需要修改它,比如隐藏在一个API下,那么就不需要重构它;如果重写比重构还容易,那么就直接重写,这需要良好的判断力和丰富的经验。 5.重构的挑战
  • 延缓新功能开发。但重构就是为了添加功能更快,修复bug更快
  • 多分支集成难度大。分支存在时间长的情况,团队每个人对代码的重构在分支合并时会有比较多的冲突
  • 可能会引入新bug。自测试的代码配合小步的重构可以很快干掉新引用的bug
  • 遗留代码重构难度大。先找到程序的接缝,插入测试,小步小步重构
  • 数据库结构的修改要特别注意。分散到多次生产发布,比如字段改名,第一次提交新增字段,暂不使用,然后同时使用两个字段,随后使用新字段,最后删除旧字段 6.重构、架构和YAGNI
  • 架构:很多人在写代码之前,必须先完成软件的设计和架构,代码一旦写出来,架构就固定了,只会因为程序员的草率对待而逐渐腐败。
  • 重构:重构改变了上述观点,重构对架构最大的影响是,通过重构,得到一个设计良好的代码库,使其能够优雅地应对不断变化的需求。不用太猜测未来需要哪些灵活性,可以根据当前的需求来构造软件。随着对用户需求的理解加深,对架构进行重构。
  • YAGNI(you aren’t going to need it 你不会需要它):平衡预先考虑架构与 重构,也就是演进式架构。 7.重构与性能
  • 重构可能使软件运行更慢,但它也使软件性能优化更容易。除了对性能有严格要求的实时系统,其他情况下都可以:先写出可调优的软件,然后调优它以求获得足够的速度。

三.代码的坏味道

  1. 神秘命名
  2. 重复代码
  3. 过长函数
  4. 过长参数列表
  5. 全局数据
  6. 可变数据
  7. 重复的switch
  8. 过大的类

四.构筑测试体系

  1. 自测代码的价值
  • 自测代码就是把期望的输出放到测试代码中,做对比,运行所有测试用例,如果一切都没问题,输出一个ok即可。相比于运行每个测试用例,逐一检查输出是否符合预期,自测代码使得测试变得像执行编译一样简单
  • 每次改动代码编译后都去执行测试,可以很快的发现代码中是否出现bug,由于频繁执行测试,bug也可以很轻松的找到,大大缩减了查找bug的时间,提高了开发效率
  1. 测试关注点
    1. 选择合适的测试框架,需要能快速知道测试是否全部通过了
    2. 风险驱动,测试那些现在或未来可能出现的bug
    3. 考虑可能出错的边界条件,把测试火力集中在那儿
    4. 测试覆盖率高低的分析只能识别出那些未被测试覆盖的代码,而不能衡量一个测试集的质量高低

五.第一组重构

  1. 提炼函数(反向重构:内联函数)
    1. 动机:将意图与实现分开,使得函数意图一眼可见
    2. 做法:
      1. 创造一个新函数,根据意图进行命名(以做什么来命名,而不是怎么做)
      2. 将待提炼的代码复制到新函数中
      3. 检查变量作用域,新函数访问不到的变量以参数的方式传递
      4. 编译,确保新函数已经妥善处理所有变量
      5. 将被提炼代码段替换为对目标函数的调用
      6. 测试
  2. 内联函数(反向重构:提炼函数)
    1. 动机:去掉无用的间接层或者重新组织函数
    2. 做法:
      1. 检查函数,确定它不具备多态性
      2. 找出这个函数的所有调用点
      3. 将这个函数的所有调用点都替换为函数本体
      4. 每次替换之后,执行测试
      5. 删除该函数的定义
  3. 提炼变量(反向重构:内联变量)
    1. 动机:使表达式简单易懂,易于管理,调试方便
    2. 做法:
      1. 确定要提炼的表达式没有副作用
      2. 声明一个不可修改的变量,把想要提炼的表达式的结果赋值给这个变量
      3. 用新变量取代原来的表达式
      4. 测试
  4. 内联变量(反向重构:提炼变量)
    1. 动机:有时变量会妨碍重构附近的代码,此时就需要通过内联的方法消除变量
    2. 做法:
      1. 检查确认变量赋值语句的右侧表达式没有副作用
      2. 如果变量没有被声明为不可修改,先将其变为不可修改,并测试,确保变量值只被赋值了一次
      3. 逐一找到使用该变量的地方,替换为直接使用赋值语句右侧表达式,并测试
      4. 删除该变量的声明点和赋值语句
      5. 测试
  5. 改变函数声明
    1. 动机:函数名改名为其用途,清晰明了;函数参数列表调整,去掉不必要的耦合、提高封装度
    2. 做法:
      1. 如果要移除参数,需要先确定函数内没有使用该参数
      2. 修改函数声明,使其符合预期
      3. 找出所有使用旧函数的地方,修改为新的函数声明
      4. 测试
  6. 封装变量
    1. 动机:重构的作用就是调整程序中的元素,重新组织数据
    2. 做法:
      1. 创建封装函数,在其中访问和更新变量值
      2. 执行静态检查
      3. 逐一修改使用该变量的代码,将其改为调用封装函数,每次替换后测试
      4. 限制变量的可见性
      5. 测试
  7. 变量改名
    1. 动机:整洁编程,名字起错了或者 程序用途随用户需求改变了
    2. 做法:
      1. 如果变量被广泛使用,考虑首先封装变量
      2. 找出所有使用该变量的代码,逐一修改
      3. 测试
  8. 引入参数对象
    1. 动机:一组数据项总是结伴同行,使用新的数据结构,参数列表能够缩短,代码一致性提升
    2. 做法:
      1. 如果暂时还没有一个合适的数据结构,就创建一个
      2. 测试
      3. 给原来的函数增加一个参数,类型是新建的数据结构
      4. 测试
      5. 调整所有调用者,传入新数据结构的适当实例,每修改一处,执行测试
      6. 用新数据结构中的每项元素逐一取代参数列表中的参数项,删除原来的参数,测试
  9. 函数组合成类
    1. 动机:发现一组函数总是操作同一块数据,就可以把这块数据作为类的数据,这组函数作为类的方法,对象内部调用函数时可以少传许多参数,简化函数调用,这样的对象也很方便传递给系统的其他部分。
    2. 做法:
      1. 首先对多个函数共用的数据加以封装
      2. 针对这些函数,将其复制到新类中
      3. 函数中处理参数的逻辑可以提炼到新类中
  10. 函数组合成变换
    1. 动机:根据基础数据计算出派生数据的逻辑可能会重复,更新时会比较费劲;可以定义专门的数据变换函数,把计算派生数据的逻辑收拢
    2. 做法:
      1. 创建一个变换函数,入参时需要变换的记录,并直接返回该记录的值(深复制保证数据不变)
      2. 挑选一块逻辑,转移到变换函数中,把结果作为字段添加到返回值中;修改客户端代码,令其使用这个新字段
      3. 测试
      4. 针对其他派生数据的逻辑,重复上述步骤
  11. 拆分阶段
    1. 动机:一段代码在同时处理两件不同的事,把它拆分成各自独立的模块,这样在需要修改的时候,就可以单独处理,不需要考虑另一个模块的细节。
    2. 做法:
      1. 将第二阶段的代码提炼成独立的函数
      2. 测试
      3. 引入中转数据结构,作为参数添加到提炼出的新函数的参数列表中
      4. 测试
      5. 逐一检查提炼出的第二阶段函数的每个参数,如果某个参数被第一阶段用到,就转移到中转数据结构,每次转移都测试
      6. 对第一阶段的代码提炼函数,返回值为中转数据结构

六.封装

  1. 封装记录
    1. 动机:记录型数据能直观地组织起存在关联的数据,把它封装起来可以隐藏结构的细节,使得使用者不必追究存储细节和计算过程
    2. 做法:
      1. 创建一个类,将记录包装起来,并将记录变量的值替换为该类的一个实例,然后在类上定义一个访问函数,返回原始记录。修改访问记录的函数,使其使用该访问函数
      2. 测试
      3. 新建一个函数,让它返回该类的对象,而非原始那条记录