前言
- 重构(refactoring):在不改变代码外在行为的前提下,对代码做出修改,以改进程序的内部结构。重构的本质就是在代码写好之后改进它的设计。
第1章 重构,第一个示例
- 如果你要给程序添加一个特性,但发现代码因缺乏良好的结构而不易于进行更改,那就先重构那个程序,使其比较容易添加该特性,然后再添加该特性。
- 需求的变化使重构变得必要。如果一段代码能正常工作,并且不会再被修改,那么完全可以不去重构它。
- 重构的第一步:确保即将被修改的代码拥有一组可靠的测试。这些测试必须有自我检验能力。
- 无论每次重构多么简单,养成重构后即运行测试的习惯非常重要。
- 对于重构过程的性能问题的处理:大多数情况下可以忽略它。如果重构引入了性能损耗,先完成重构,再做性能优化。
- 编程时,需要遵循营地法则:保证你离开时的代码库一定比来时更健康。
- 好代码的检验标准就是人们是否能轻而易举地修改它。
第2章 重构的原则
- 重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
- 重构(动词):使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。
- 如果有人说他们的代码在重构过程中有一两天时间不可用,基本上可以确定,他们在做的事不是重构。
- “结构调整”(restructuring)泛指对代码库进行的各种形式的重新组织或清理,重构则是特定的一类结构调整。
- 使用重构技术开发软件时,添加新功能和重构,同一时间,二者只能做其一。
- 为何重构:重构改进软件的设计;重构使软件更容易理解;重构帮助找到bug;重构提高编程速度。
- 何时重构:预备性重构(添加新功能之前重构);帮助理解的重构(提升代码阅读性);捡垃圾式重构(消除代码冗余等);有计划的重构(固定周期对之前写过的代码重构)和见机行事的重构(写新代码过程中发现其他代码需要重构);长期重构(更新三方库等);代码复审时重构。
- 不必将重构与添加新功能在版本控制的提交中分开。原因:重构常因新功能所致,单独提交脱离重构的上下文,体现不出为何需要重构;重构的代码和新功能代码高耦合。
- 如果经理不能接受代码重构需要花时间,那就不要给经理说。
- 何时不应该重构:凌乱且不需要修改的代码;重写比重构更容易的代码。
- 重构的挑战:延缓新功能开发;代码所有权(代码是其他部门写的);分支(多分支开发,最后合并代码);测试(代码测试不完善,无法保证重构后不引入新BUG);遗留代码;数据库(改字段值)。
- 重构的唯一目的就是让我们开发更快,用更少的工作量创造更大的价值。
- “写代码之前就完成软件的设计和架构”这个思想是不正确的。因为需求会变,且一开始的考虑而不是完全的周全。
- 三大实践:自测试代码、持续集成、重构。
- 用性能测试工具去定位性能问题,而不是根据对代码的了解来猜测。
- 一些语言(Java、C#等)可以借助工具进行自动化的重构。
第3章 代码的坏味道
- 当你”闻到”代码中有坏味道时,就表明代码需要进行重构了。
- 当你感觉需要撰写注释时,请先尝试重构,试着让所有注释都变得多余。
- 神秘命名。
- 重复代码。
- 过长函数。
- 过长参数列表。
- 全局数据。(PS:慎用单例模式)
- 可变数据(如在js中使用Immutable.js)。
- 发散式变化(违背了单一职责原则)。
- 霰弹式修改(需求变化是,需要在很多个地方做修改。封装性不够好)。
- 依恋情结(违背了依赖倒置原则)。
- 数据泥团(很多类中的变量名相同,考虑抽象出基类)。
- 基本类型偏执(针对问题域定义新的类型而不是用基本类型,比如定义坐标类,而不是用std::pair)。
- 重复的switch。
- 循环语句。
- 冗赘的元素(有些类结构是不必要的)。
- 夸夸其谈通用性。
- 临时字段(类内部某个字段仅为某种特定情况而设)。
- 过长的消息链。
- 中间人(过度使用委托)。
- 内幕交易(用委托替代继承等)。
- 过大的类。
- 异曲同工的类。
- 纯数据类(使用面向对象编程而不是面向过程)。
- 被拒绝的遗赠(子类复用了超类的实现,却又不愿意支持超类的接口,那就不要用继承)。
- 注释(过长的注释)。
第4章 构筑测试体系
- 稳固的测试集合是正确地进行重构的前提条件之一。
- 确保所有测试都完全自动化,让它们检查自己的测试结果。
- 一套测试就是一个强大的bug侦测器,能够大大缩减查找bug所需的时间。
- 测试驱动开发的编程方式依赖于下面这个短循环:先编写一个(失败的)测试,编写代码使测试通过,然后进行重构以保证代码整洁。
- 总是确保测试不该通过时真的会失败。
- 频繁地运行测试。对于你正在处理的代码,与其对应的测试至少每隔几分钟就要运行一次,每天至少运行一次所有的测试。
- 编写未臻完善的测试并经常运行,好过对完美测试的无尽等待。
- 考虑可能出错的边界条件,把测试火力集中在那儿。
- 如果错误会导致脏数据在应用中到处传递,或是产生一些很难调试的失败,考虑引入断言手法,使代码不满足预设条件时快速失败。不用为这样的失败断言添加测试,它们本身就是一种测试的形式。
- 不要因为测试无法捕捉所有的bug就不写测试,因为测试的确可以捕捉到大多数bug。
- 每当你收到bug报告,请先写一个单元测试来暴露这个bug。
第5章 介绍重构名录
- 书的剩余部分是重构方法的索引,作为参看书来使用。
第6章 第一组重构
- 提炼函数 vs 内联函数。
- 提炼变量 vs 内联变量。
- 改变函数声明。
- 封装变量。
- 变量改名。
- 引入参数对象。
- 函数组合成类v
- 函数组合成变换。
- 拆分阶段。
第7章 封装
- 封装记录。
- 封装集合。
- 以对象取代基本类型。
- 以查询取代临时变量。
- 提炼类 vs 内联类。
- 隐藏委托关系 vs 移除中间人。
- 替换算法。
第8章 搬移特性
- 搬移函数。
- 搬移字段。
- 搬移语句到函数 vs 搬移语句到调用者。
- 移动语句。
- 拆分循环。
- 以管道取代循环。
- 移除死代码。
第9章 重新组织数据
- 拆分变量。
- 字段改名。
- 以查询取代派生变量。
- 将引用对象改为值对象 vs 将值对象改为引用对象。
第10章 简化条件逻辑
- 分解条件表达式 vs 合并条件表达式。
- 以卫语句取代嵌套条件表达式(Replace Nested Conditional with Guard Clauses)。
- 以多态取代条件表达式。
- 引入特例。
- 引入断言。
第11章 重构API
- 将查询函数和修改函数分离。
- 函数参数化。
- 移除标记参数。
- 保持对象完整。
- 以查询取代参数 vs 以参数取代查询。
- 移除设值函数。
- 以工厂函数取代构造函数。
- 以命令取代函数 vs 以函数取代命令。
第12章 处理继承关系
- 函数上移 vs 函数下移。
- 字段上移 vs 字段下移。
- 构造函数本体上移。
- 以子类取代类型码 vs 移除子类。
- 提炼超类。
- 折叠继承体系。
- 以委托取代子类。
- 以委托取代超类。