1 重构,第一个示例
- 重构前,先检查自己是否有一套可靠的测试集。这些测试必须有自我验证能力。TDD
- 重构技术就是以微小的步伐修改程序。如果犯下错误,很容易便可发现它。
- 傻瓜都能写出计算机可以理解的代码。唯有能写出人类容易理解的代码的,才是优秀的程序员。
- 编程时,需要遵循营地法则:保证你离开时,代码库一定比来的时候更健康。
- 好代码验证的标准是人们是否能轻而易举的修改它。
2 重构的原则
2.1 何谓重构
- 重构:对软件内部结构的一种调整,目的是在***不改变可观察行为的前提下***,提高其可理解性,降低其修改成本。
- 重构的关键在于运用大量小且保证软件行为的步骤,一步步达到大规模的修改。
- 如果有人说他们的代码在重构过程中有1-2天时间不可用,基本上可以确定,他们在做的事不是重构。
2.2 两顶帽子
- 添加新功能,可能需要优化之前的程序结构;当功能开发好,也需要优化下程序的结构。不同的角色切换,是在添加功能过程中必不可少的步骤。
2.3 为何重构
- 改进软件的设计
- 使软件更容易理解
- 帮助开发者找到bug
- 提高编程速度
2.4 何时重构
- 预备性:让添加新功能更容易
- 帮助理解:使代码更易懂
- 捡垃圾式重构
- 有计划和见机行事的重构:肮脏的代码必须重构,但漂亮的代码也需要很多重构
- 长期重构
- 复审代码时重构
- 何时不应该重构:
- 只有当需要理解其工作原理时
- 如果重写比重构容易
2.5 重构的挑战
- 缓解新功能开发
- 重构的唯一目的就是让我们开发更快,用更少的工作量创造更大的价值
- 重构应该总是由经济利益驱动,而不是在于把代码库打磨得闪闪发光
- 分支
- 持续集成,也叫基于主干开发,避免任何分支彼此差距太大,从而降低合并的难度
- 测试
- 自测试代码:快速发现错误
- 遗留代码
- 重构可以很好地帮助我们理解遗留系统,但遗留的系统大多数是没有测试。解决办法是:没测试就加测试。书籍推荐《修改代码的艺术》
2.6 重构、架构和YAGNI
- 一旦代码写出来,架构就固定了,只会因为程序员的草率对待而逐渐腐败,重构可以改变这个状态
- 重构可以应对未来的需求变化
2.7 重构与软件开发过程
- 极限编程是最早的敏捷软件开发方法之一。要真正以敏捷的方式运作项目,团队成员必须在重构上有能力、有热情,他们采用的开发过程必须与常规的、持续的重构相匹配
- 自测试代码 -> 持续集成 -> 重构
2.8 重构与性能
- 除了对性能有严格要求的实时系统,其他情况下,“编写快速软件”的秘诀是:先写出可调优的代码,然后调优它以求获得足够的速度
- 短期看,重构可能会让软件变慢,但它的优化阶段的软件性能调优更容易,最终还是会得到好的效果
2.9 重构起源何处
- 优秀的程序员肯定会花一些时间来清理自己的代码,因为他们确定自己几乎无法一开始就写出整洁的代码
2.10 自动化重构
- 强大的IDE会让重构变得很轻松
3 代码的坏味道
- 神秘命名:如果想不出一个好的名字,说明背后很可能隐藏着更深的设计问题
- 重复代码:优化:对比差异,提取相同。
- 过长函数:优化:条件、循环、公共集中的过程提取处理
- 过长参数列表:优化:使用对象合并参数
- 全局数据:优化:合并数据到方法、类成员中
- 可变数据:优化:函数式编程、数据永不改变
- 发散式变化:做出的某个模块的小修改,必须修改某个类的多个函数。优化:每次只关心一个上下文,将联动的改变提取处理
- 霰弹式修改:每次遇到变化,都必须在很多不同的类内做出许多小修改。优化:提取公共方法
- 依恋情结:如果一个函数跟另一个模块中的函数或者数据交流格外频繁,远胜于在自己所处的模块内部交流
- 模块化:力求将代码分出区域,最大化区域内部的交互、最小化跨区域的交互。所谓高内聚,低耦合
- 数据泥团:两个类中相同的字段、许多函数签名中相同的参数。优化:提取公共字段为一个类、对象
- 基本类型偏执:一些基本类型无法表示一个数据的真实意义,例如电话 码、温度等。优化:使用类、对象字符串类型变量取代基本类型
- 重复的 :优化:使用策略模式、提取子类
- 循环语句:同过长函数
- 冗赘的元素:优化:内联、删除
- 夸夸其谈通用性:优化:内联、删除
- 临时字段:优化:内联、删除
- 过长的消息链:优化:减少委托关系
- 中间人:优化:用继承替代代理委托
- 内幕交易:优化:合并相同的联系,提取不同的成分
- 过大的类:优化:提取类、子类、接口
- 异曲同工的类:优化:提取公共类、使用子类继承
- 纯数据类:它们拥有一些字段,以及访问、读写这些字段的函数。优化:将相关操作封装进去,降低 成员变量
- 被拒绝的遗赠:如果子类继承超类的数据和方法,但不使用。优化:用内联数据和方法、代理委托替代继承关系
- 注释:当你感觉需要编写注释时,请先尝试重构,试着让所有注释变得多余
4 构筑测试体系
4.1 自测试代码的价值
- 程序员编写代码的时间仅占所有时间中很少的一部分,但是花费在调试上的时间是最多的。修复bug通常是比较快的,但找出bug所在却是一场噩梦
- 确保所有测试都是完全自动化,让他们检查自己的测试结果
- 一套测试就是一个强大的bug侦探器,能够大大缩减查找bug所需的时间
4.2 测试代码示例
4.3 第一个测试
- 总是确保测试不该通过时,会产生失败
- 频繁地运行测试,对于你正在处理的代码与其对应的测试至少每隔几分钟就要运行一次,每天至少运行一次所有的测试
4.4 再添加一个测试
- 编写为臻完善的测试并经常运行,好过对完美测试的无尽等待
- 保持每个测试用例独立性,避免产生共享对象。因为测试之间会通过共享产生交互,而测试的结果就会受测试运行次序的影响,导致测试结果的不确定性
- 例子 change to
4.5 修改测试夹具
- 配置 – 检查 – 验证
- 准备 – 行为 – 断言
4.6 探测边界条件
- 考虑可能出错的边界条件,把测试火力集中在那儿
- 不要因为测试无法捕捉所有的bug就不写测试,因为测试的确可以捕捉到大多数bug
- 任何测试都不能证明一个程序没有bug
- 当测试数量达到一定程度后,继续增加测试代理的边际效用会递减
- 应该把测试集中在可能出错的地方,观察代码,看哪儿变得复杂、哪些地方可能出错
4.7 测试远不止如此
- 一个架构的好坏,很大程度上要取决于它的可测试性,这是一个好的行业趋势
- 每当收到bug 告,请先写一个单元测试来暴露这个bug
- 一个测试集是否够好,最好的衡量标准其实是主观的,试问自己:如果有人在代码里引入了一个缺陷,自己有多大的自信它能被测试集发现
5 介绍重构名录
6 第一组重构
6.1 提炼函数
-
对立:内联函数
-
目的:将意图与实现分开。意图 == 主干;实现 == 分支的实现
-
场景:如果需要花时间浏览一段代码才能弄清它到底干什么,那么就应该将其提炼到一个函数中,并根据它所做的事为其命名。以后再读到这段代码时,可以一眼就能知道函数的用途,大多数根本不需要关心函数如何实现。
-
例子:
6.2 内联函数
-
对立:提炼函数
-
目的:去除不必要间接层/委托层,降低系统复杂度
-
场景:
- 一堆不合理的函数,可以将其内联到一个大型函数中,再重新提炼到小函数中
- 代码太多间接层,使系统中的所有函数都似乎只是对另一个函数简单的委托
-
例子:
6.3 提炼变量
-
对立:内联变量
-
目的:将复杂的表达式使用变量说明
-
例子:
6.4 内联变量
-
对立:提炼变量
-
目的:去除不必要变量
-
场景:
- 表达式比变量更有表现力
-
例子:
6.5 改变函数声明
-
目的:好名字能让人一眼看出函数的用途,而不必看代码实现
-
使用:
- 先写一句注释描述这个函数的用途,再把这句注释变成函数的名字
-
例子:
6.6 封装变量
-
目的:
- 重构数据转移为重构函数,更易于处理
- 监控数据的变化
-
场景:如果数据的可访问范围大
-
例子:
6.7 变量改名
-
目的:好名字可让上下文更清晰
-
例子:
6.8 引入参数对象
-
目的:组织数据结构,让数据项之间的关系更清晰,参数列表也能缩短
-
场景:一个函数接受多个参数
-
例子:
6.9 函数组合成类
-
目的:
- 对象内部调用这些函数可以少传参数,从而简化函数调用,而且一个对象可更方便传递给系统的其他部分
- 客户端修改对象的核心数据,通过计算得出的派生数据会自动与核心数据保存一致
-
场景:如果一组函数形影不离地操作同一块数据(通常是将这块数据作为参数传递给函数)
-
例子:
function base(reading){}function taxableCharge(reading){}function calcBaseCharge(reading){}
声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!