从重构中重拾编程的乐趣 - 再读《重构》

2019-07-27

这本书在五年前读了一次,当时读完觉得自己的水平上了一个台阶,然后开始在生产项目中实践。当时的项目是一堆没人维护的遗留代码,每当要做个新功能时,我都会重构(更准确的说法是重写)下与新功能相关的逻辑,因为没有测试用例的支撑,经常会因为改出问题导致自己加班。当时我从这种修改代码的过程中找到了编程的乐趣,那是一种畅快淋漓的感觉,重构后的代码似乎也成了体现我个人编程水平的象征。

随着写的代码越来越多,维护项目中的这些代码耗费了我太多的时间,想写新的功能时发现自己无法抽身出来。我渐渐地感觉到代码成了一种负债,写的越多负债越多,我被自己的写的代码给困住了。

近期重读《重构》这本书,深感以前的我在重构方面至少犯了三个错误。

  1. 重构前没写单元测试。这样的重构很容易给自己挖坑,本想改个函数名,结果却捅了个蚂蜂窝。每次重构都觉得不踏实,生怕改错了什么地方
  2. 添加新特性和重构同时进行,最后一起测试。这样容易搞混原有功能与新特性。一起测试意味着没有单独对重构的代码进行小范围的测试,让代码集成测试起来复杂
  3. 为了重构而重构。很多时候重构成了一种强迫症,还美其名曰说自己有代码洁癖

代码不仅仅是写完就好了,还需要维护。维护说白了就是让代容易理解,让代码易于扩展修改成本低。容易理解的代码可以很方便的交接出去给其它同事维护,难理解的代码就只能砸在自己手里。一旦有缺陷或者新功能,修改成本低易扩展的代码可以让你很快就完成需求,收获同事的佩服,早点下班。让代码易于维护就得借助重构来完成。

近些年在项目中逐渐被“剥夺”了写代码的权利,看代码和定位问题的时间超过了写代码,从工作中得到的乐趣少了很多。希望重拾重构,从编程中找回那久违的畅快淋漓。

读书笔记

关于重构

  1. 什么是重构?
    • 不改变软件可观察行为的前提下,提高可理解,降低软件维护成本
  2. 为什么重构?
    • 可以改进软件设计。代码被阅读和被修改的次数远远多于它被编写的次数
    • 可以使软件更容易理解。“擦掉窗户上的污垢,使你看得更远”
    • 可以帮助找到 bug
    • 可以提高编程速度
  3. 何时重构?
    • 随时随地都可以重构,不要为重构而重构
    • 三次法则
    • 添加功能时先重构
    • 修复缺陷时重构
    • 审查代码时重构
    • 代码中出现坏味道时,进行重构
  4. 何时不重构?
    • 代码无法运行时不应重构
  5. 重构与其它
    • 重构改变了预先进行代码设计的角色。通过重构你可以找出改变中的平衡点,你会发现所谓设计不再是一切动作的前提,而是在整个开发过程中逐渐浮现出来
    • 短期看,重构可能使软件性能变慢,但它使优化阶段的软件性能调整变得更加容易

重构要点

  1. 当你为程序添加功能不是很方便时,先重构使功能添加比较容易进行,然后再添加功能。重构与添加新功能不应该同时进行
  2. 重构前,先建立待修改代码的单元测试用例
  3. 重构就是以微小的步伐修改程序。如果过程中出错,会很容易发现
  4. 重构:使用一系列重构方法,对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高可理解性,降低其修改成本
  5. 任何一个人都可以写出计算机可以理解的代码。唯有写出人类容易理解的代码,才是优秀的程序员
  6. 事不过三,三则重构
  7. 当你感觉需要给代码写注释时,请先重构,试着让所有注释都变得多余
  8. 确保所有测试都自动化,让它们检查自己的测试结果。频繁运行测试
  9. 一套测试就是一个强大的 bug 探测器,能够大大缩减查找 bug 所需要的时间
  10. 每当你收到 bug 时,请先写一个单元测试来暴露这个 bug
  11. 考虑可能出错的边界条件,集中火力测试它们
  12. 不要因为测试无法捕捉所有 bug 就不写测试,因为测试确实可以捕捉大部分 bug
  13. 闻到代码的坏味道时进行重构
  14. 看一遍重构列表,需要时对照着做

22 种常见的代码坏味道

当代码中出现以下坏味道,你应该对代码进行重构。

  1. 重复代码。合并重复代码。
  2. 过长函数。程序越长越难理解,而且不好复用。这时要做的是分解函数,怎么分解?当你觉得要写点注释来说明时,可以把这部分代码放到独立函数里,并以它的用途(而非实现手法)命名。
  3. 过大的类。过大的类往往有太多的实例变量,容易导致很多重复代码,同时说明这个类责任太大了,需要拆解。
  4. 过长参数列。我们往往为了规避使用全局数据而添加了过多的参数,但这会让函数难以理解、不容易使用。可以考虑用对象来包裹参数。
  5. 发散式变化。如果某一外界的变化会导致某个类多处需要修改,那么应该考虑把这个类拆分成两个或更多,这样每个类就可以只因为一种变化而需要修改。
  6. 霰弹式修改。如果每遇到某种变化,你都必须在许多不同的类内做许多小修改,那么应该考虑把这一系列相关的修改点放进同一个类中。
  7. 依恋情结。函数对某个类的(通常是数据)兴趣高过对自己所处类。比如某函数为了计算某个值,从另一对象那里调用了几乎一半的取值函数,那么可以考虑把函数移出去。总的原则是:将总是一起变化的东西放在一块儿。
  8. 数据泥团。经常同时出现的数据项就是数据泥团,这些绑在一起的数据应该拥有它们自己的对象。
  9. 基本类型偏执。对象模糊了基本数据与体积较大的类之间的界限。多用对象,少用基本类型。
  10. switch 语句。switch 语句会带来重复,考虑用多态替换它。
  11. 平行继承关系。每当你为某个类增一个子类时,必须也为另一个类相应增加一个子类,那么你应该考虑调整继承体系来消除这种重复性。
  12. 冗余类。没有用到的代码和类,记得删掉。
  13. 夸夸奇谈未来性。为将来某一天需要用到而增加的非必要代码,尽量移除掉。
  14. 令人迷惑的暂时字段。因某种特定情况而增加的实例变量会让人迷惑,应移除。
  15. 过度耦合的消息链。对象 A 请求 B,B 请求 C ,C 请求 D ...这种的消息链会产生很多临时变量,也意味着客户代码将与查找过程中的导航结构紧密耦合,会造成修改困难。
  16. 中间人。过度运用委托可能会导致某个类接口有一半的函数都委托给了其它类。这时应该减少中间类,直接与真正负责的对象打交道。
  17. 狎昵关系的代码。两个类过于亲密,花费太多时间去探究彼此的 private 成分,这样的类应该拆散。
  18. 异曲同工的类。合并那些不同方法签名但做同样事情的函数。
  19. 不完美类库。类库并不一定能完全满足你的需求,这时我们需要采用一些重构手法完善它。
  20. 纯粹的数据类。应该把这些数据像对象一样封装起来,而不是直接暴露 public 字段。
  21. 被拒绝的遗赠。子类可能不想或不需要继承超类的函数和数据,这可能是继承体系设计错误。
  22. 过多的注释。注释应该用来标记为什么做某事,而不是做了某事。尽可能地让合理的函数名来替换注释。

72 条重构列表

书中重构列表总共 72 条,分为 7 个主题,相应的章节对这些重构方法有整体概述。建议把这 72 条重构动机、做法和范例都看一遍有个印象,当需要重构时对照着做。

  1. 重新组织函数。这些重构手法主要处理过长函数、缩减函数中的临时变量、处理函数中的参数和改进函数中的算法。
    • 提炼函数
    • 内联函数
    • 内联临时变量
    • 以查询取代临时变量
    • 引入解释性变量
    • 分解临时变量
    • 移除对参数的赋值
    • 以函数对象取代函数
    • 替换算法
  2. 在对象之间搬移特性。这些重构手法为了明晰类的责任。
    • 搬移函数
    • 搬移字段
    • 提炼类
    • 将类内联化
    • 隐藏“委托关系”
    • 移除中间人
    • 引入外加函数
    • 引入本地扩展
  3. 重新组织数据。这些重构手法主要是使用对象管理数据,以及对类型码这类特殊数据的处理。
    • 自封装字段
    • 以对象取代数据值
    • 将值对象改为引用对象
    • 将引用对象改为值对象
    • 以对象取代数组
    • 复制“被监视数据”
    • 将单向关联改为双向关联
    • 将双向关联改为单向关联
    • 以字面常量取代魔法数
    • 封装字段
    • 封装集合
    • 以数据类取代记录
    • 以类取代类型码
    • 以子类取代类型码
    • 以状态或策略模式取代类型码
    • 以字段取代子类
  4. 简化条件表达式,这些重构手法主要是分离分支逻辑和操作细节,合并重复判断,用多态等方式替换多重判断,使用卫语句提早结束判断,包装空对象减少判断。
    • 分解条件表达式
    • 合并条件表达式
    • 合并重复的条件片段
    • 移除控制标记
    • 以卫语句取代嵌套条件表达式
    • 以多态取代条件表达式
    • 引入Null 对象
    • 引入断言
  5. 简化函数调用。这些重构手法主要让接口变得更简洁。
    • 函数改名
    • 添加参数
    • 移除参数
    • 将查询函数与修改函数分离
    • 令函数携带参数
    • 以明确函数取代参数
    • 保持对象完整
    • 以函数取代参数
    • 引入参数对象
    • 移除设值对象
    • 隐藏函数
    • 以工厂函数取代构造函数
    • 封装向下转型
    • 以异常取代错误码
    • 以测试取代错误码
  6. 处理继承关系。这些重构手法主要为了让类的继承更加清晰合理。
    • 字段上移
    • 函数上移
    • 构造函数上移
    • 函数下移
    • 字段下移
    • 提炼子类
    • 提炼超类
    • 提炼接口
    • 折叠继承体系
    • 塑造模板函数
    • 以委托取代继承
    • 以继承取代委托
  7. 大型重构。这些重构手法不同前面的小步伐重构,它的粒度更大。
    • 梳理并分解继承体系
    • 将过程化设计转化为对象设计
    • 将领域与显示分离
    • 提炼继承体系
Comments
Write a Comment