《重构:改善既有代码的设计》- 笔记
1. 概述性章节
- 如果你要给程序添加一个特性,但发现代码因缺乏良好的结构而不易于进行更改,那就先重构那个程序,使其比较容易添加该特性,然后再添加该特性。
- 需求的变化使重构变得必要。如果一段代码能正常工作,并且不会再被修改,那么完全可以不去重构它。
- 重构第一步:重构前,先检查自己是否有一套可靠的测试集。这些测试必须有自我检验能力。
- 重构过程的精髓所在:小步修改,每次修改后就运行测试。重构技术就是以微小的步伐修改程序。如果你犯下错误,很容易便可发现它。
- 傻瓜都能写出计算机可以理解的代码。唯有能写出人类容易理解的代码的,才是优秀的程序员。
- 临时变量往往会带来麻烦。它们只在对其进行处理的代码块中有用,因此临时变量实质上是鼓励你写长而复杂的函数。
- 好的变量命名很重要。重构过程中想到好的名字随时修改。
- 如果重构引入了性能损耗,先完成重构,再做性能优化。
- 把复杂的代码块分解为更小的单元,与好的命名一样都很重要。
- 先重构代码逻辑结构,再重构功能部分。
- 一般来说,重构早期的主要动力是尝试理解代码如何工作。通常你需要先通读代码,找到一些感觉,然后再通过重构将这些感觉从脑海里搬回到代码中。
2. 何为重构
-
重构作用:
- 改进软件设计。消除重复代码
- 是软件容易理解
- 帮助找到 BUG
- 提高再编程速度
-
何时重构
- 如果想替换掉一个正在使用的库,可以先引入一层新的抽象,使其兼容新旧两个库的接口。一旦调用方已经完全改为使用这层抽象,替换下面的库就会容易得多。
- 长期重构,对大型项目
- 审查代码时重构
- 有时先重构再添加新功能会更快些
- 只有当我需要理解其工作原理时,对其进行重构才有价值。如果重写比重构还容易,就别重构了。
-
重构的挑战
- 延缓新功能开发:如果你是一支团队的技术领导,一定要向团队成员表明,你重视改善代码库健康的价值。
- 代码所有权:推荐团队代码所有制,这样一支团队里的成员都可以修改这个团队拥有的代码
- 分支:分支存在的时间尽量短,合并时才不麻烦
- 测试:
- 一般来说,只有在设计系统时就考虑到了测试,这样的系统才容易添加测试
- 添加测试:建议你先找到程序的接缝,在接缝处插入测试,如此将系统置于测试覆盖之下。
-
重构过程
- 自己重构需要有测试代码。
- 团队重构需要有 CI。
- 哪怕你完全了解系统,也请实际度量它的性能,不要臆测。
-
程序性能
- 关于性能,一件很有趣的事情是:如果你对大多数程序进行分析,就会发现它把大半时间都耗费在一小半代码身上。
-
自动化重构
- 有些 IDE 集成自动化重构工具。
- 简单的工具是查找/替换
- 还有变量,函数调用结构(语法树)的分析工具
3. 哪些代码要重构
- 神秘命名:改名可能是最常用的重构手法,包括改变函数声明(124)(用于给函数改名)、变量改名(137)、字段改名(244)等。
- 重复代码:采用提炼函数(106)提炼出重复的代码,然后让这两个地点都调用被提炼出来的那一段代码。
- 过长函数:每当感觉需要以注释来说明点什么的时候,我们就把需要说明的东西写进一个独立函数中,并以其用途(而非实现手法)命名。
- 对于庞大的 switch 语句,其中的每个分支都应该通过提炼函数(106)变成独立的函数调用。
- 条件表达式和循环常常也是提炼的信号。
- 应该将循环和循环内的代码提炼到一个独立的函数中。
- 过长参数列表:
- 全局变量:把全局数据用函数包装起来,控制对它的访问。
- 可变数据:封装变量(132)来确保所有数据更新操作都通过很少几个函数来进行,使其更容易监控和演进;
- 一个变量在不同时候被用于存储不同的东西,可以使用拆分变量(240)将其拆分为各自不同用途的变量;
- 将查询函数和修改函数分离; - 发散式变化:
- 散弹式变化:如果每遇到某种变化,你都必须在许多不同的类内做出许多小修改,你所面临的坏味道就是霰弹式修改。
- 依恋情节:一个函数跟另一个模块中的函数或者数据交流格外频繁,远胜于在自己所处模块内部的交流,这就是依恋情结的典型情况;判断哪个模块拥有的此函数使用的数据最多,然后就把这个函数和那些数据摆在一起。
- 数据泥团:两个类中相同的字段、许多函数签名中相同的参数。这些总是绑在一起出现的数据真应该拥有属于它们自己的对象。
- 基本类型偏执:创建对自己的问题域有用的基本类型,如钱、坐标、范围等。赋予变量实际意义。
- 重复的 switch: 使用多态替代 switch
- 循环语句:
- 冗余的元素:
- 临时字段:
- 过长的消息链:
- 中间人:
- 内幕交易:
- 过大的类:
- 异曲同工的类
- 纯数据类:
- 被拒绝的遗赠:
- 注释:
4. 构筑测试体系
- 确保所有测试都完全自动化,让它们检查自己的测试结果。
- 考虑可能出错的边界条件,把测试火力集中在那儿。
- 如果这个错误会导致脏数据在应用中到处传递,或是产生一些很难调试的失败,我可能会用引入断言(302)手法,使代码不满足预设条件时快速失败。我不会为这样的失败断言添加测试,它们本身就是一种测试的形式
- 不要因为测试无法捕捉所有的 bug 就不写测试,因为测试的确可以捕捉到大多数 bug。
- 每当你收到 bug 报告,请先写一个单元测试来暴露这个 bug
6. 介绍重构名录
- 提炼函数:如果你需要花时间浏览一段代码才能弄清它到底在干什么,那么就应该将其提炼到一个函数中,并根据它所做的事为其命名。以后再读到这段代码时,你一眼就能看到函数的用途,大多数时候根本不需要关心函数如何达成其用途(这是函数体内干的事)。
- 创造一个新函数,根据这个函数的意图来对它命名(以它“做什么”来命名,而不是以它“怎样做”命名)。
- 内联函数:
- 提炼变量:
- 内嵌变量:
- 改变函数声明:修改参数列表不仅能增加函数的应用范围,还能改变连接一个模块所需的条件,从而去除不必要的耦合。
- 封装变量:对于所有可变的数据,只要它的作用域超出单个函数,我就会将其封装起来,只允许通过函数访问。数据的作用域越大,封装就越重要。不可变性是强大的代码防腐剂。
- 变量改名:变量的用途在这个上下文中很清晰。
- 引入参数对象:使用数据结构代替多个数据参数。将数据组织成结构。
- 函数合成类:C 中可以将函数指向一个结构体变量。
- 函数组合成变换:
- 拆分阶段:每当看见一段代码在同时处理两件不同的事,我就想把它拆分成各自独立的模块。就是把一大段行为分成顺序执行的两个阶段。
7. 封装
- 封装记录
- 封装集合:封装程序中的所有可变数据。这使我很容易看清楚数据被修改的地点和修改方式,这样当我需要更改数据结构时就非常方便。
- 以对象取代基本类型:
- 以查询取代临时变量:
- 提炼类:
- 内联类:
- 移除中间人:
- 替换算法:
9. 重新组织数据
- 拆分变量:如果变量承担多个责任,它就应该被替换(分解)为多个变量,每个变量只承担一个责任。同一个变量承担两件不同的事情,会令代码阅读者糊涂。
10. 简化条件逻辑
- 对于条件逻辑,将每个分支条件分解成新函数还可以带来更多好处:可以突出条件逻辑,更清楚地表明每个分支的作用,并且突出每个分支的原因。
- 合并条件表达式:
- 使用断言:它们告诉阅读者,程序在执行到这一点时,对当前状态做了何种假设。
11. 重构API
- 将查询函数和修改函数分离
- 函数参数化:如果我发现两个函数逻辑非常相似,只有一些字面量值不同,可以将其合并成一个函数,以参数的形式传入不同的值,从而消除重复。
- 移除标记参数:“标记参数”是这样的一种参数:调用者用它来指示被调函数应该执行哪一部分逻辑
- 保持对象完整性:如果我看见代码从一个记录结构中导出几个值,然后又把这几个值一起传递给一个函数,我会更愿意把整个记录传给这个函数,在函数体内部导出所需的值。如果将来被调的函数需要从记录中导出更多的数据,我就不用为此修改参数列表。并且传递整个记录也能缩短参数列表,让函数调用更容易看懂。
- 以查询取代参数:函数的参数列表应该总结该函数的可变性,标示出函数可能体现出行为差异的主要方式。和任何代码中的语句一样,参数列表应该尽量避免重复,并且参数列表越短就越容易理解
- 移除设值函数:如果不希望在对象创建之后此字段还有机会被改变,那就不要为它提供设值函数