《重构》

第一章 重构,第一个示例

重构的第一步

  • 重构前,先检查自己是否有一套可靠的测试集。这些测试必须有自我检验的能力。

    重构过程的精髓

  • 重构过程的精髓,小步修改,每次修改后就运行测试。(书里可以看到,每次,甚至只有变量改动的时候,都会执行编译、测试、提交)。

    作者的命名习惯

    命名习惯:永远将函数的返回值命名为result,使用动态类型语言时,为参数取名时默认带上类型名。

    重构与性能优化

  • 如果重构引入了性能损耗,先完成重构,再做性能优化。

    重构测试失败

    当重构过程有测试失败,而又无法马上看清问题所在并立即修复时,回滚到最后一次可工作的一觉,然后以更小的步子重做

疑问:

A:以查询代替临时变量(178)配合内联变量(123)时(P12),虽然减少了临时变量的数量,但是增多了一个方法,这真的有必要吗?
Q:「移除局部变量的好处是做提炼时会简单得多,因为需要操心的局部作用域变少了」

A:甚至为了减少局部变量,或者说局部变量的声明更显式,增加了一步循环…………
Q:「我知道很多人本能的警惕重复的循环,但大多数时候,重复一次这样的循环对性能的影响都可忽略不计。…软件的性能通常只与代码的一小部分相关,改变其他的部分汪汪队整体性能贡献甚微。…因为有了一份结构良好的代码,回头调优其性能也容易的多」

第二章 重构的原则

名词解释

  • 重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
  • 重构(动词):使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。
  • 可观察行为:重构之后的代码所做的事应该与重构之前大致一样。

摘抄

重构在于小步迭代

重构的关键在于运用大量微小且保持软件行为的步骤,一步步达成大规模的修改。每个单独的重构要么很小,要么由若干小步骤组合而成。因此,在重构的过程中,我的代码很少进入不可工作的状态。即使重构没有完成,我也可以在任何时刻停下来。

重构与性能优化

重构是为了让代码“更容易理解,更易于修改”。这可能让程序运行的更快,也可能让程序运行的更慢。 在性能优化时,我只关心让程序运行的更快,最终得到的代码可能更难理解和维护。

重构的目的

  1. 重构改进软件的设计:消除重复代码
  2. 重构使软件更容易理解:理解并不只是对计算机,还应该是对另一个程序员,或者未来的你自己。
  3. 重构帮助找到bug:更加深入的理解代码的所作所为
  4. 重构提高编程速度:通过投入精力改善内部设计,我们增加了软件的耐久性,从而可以更长时间的保持开发的快速。

何时重构

  • 预备性重构:让添加新功能更容易。
    • 在开始开发新功能之前,先思考现有的结构是否满足新功能的需求,不满足就进行重构。
  • 帮助理解的重构:使代码更易懂
  • 捡垃圾式重构:我已经理解代码在做什么,但发现他做的不好。
  • 有计划的重构和见机行事的重构

    每次要修改时,首先令修改很容易(警告,这件事有时会很难),然后在进行这次容易的修改。 ———— Kent Beck

  • 长期重构(不要破坏代码结构,保持过去的功能可用)

  • 复审代码时重构

何时不应该重构

  • 如果我看见一块凌乱的代码,但并不需要修改它。
  • 如果丑陋的代码被隐藏在一个API下,我就可以忍受它继续保持丑陋,只有当我需要理解其工作原理时,对其进行重构才有价值。
  • 重写比重构还容易时。

重构的意义

重构的意义不在于把代码库打磨的闪闪发光,而是纯粹经济角度出发的考量。 我们之所以重构,是因为它能让我们更快——添加功能更快,修复bug更快。 重构应该总是由经济利益驱动。

面对遗留的代码时

  • 遗留系统缺少测试。
  • 买一本《修改代码的艺术》。总结:建议先找到程序的接缝,在接缝处插入测试,如此将系统置于测试覆盖之下。

YAGNI/简单设计/增量式设计

与其猜测未来需要哪些灵活性、需要什么机制来提供灵活性,我更愿意只根据当前的需求来构造软件,同时把软件的设计质量做的很高。随着对用户需求理解的加深,我会对架构进行重构,使其能够应对新的需要, YAGNI并不是完全不用预先考虑架构,而是将架构、设计与开发过程融合的一种工作方式,这种工作方式必须有重构作为基础才可靠。

YAGNI设计方法的三大实践

  • 自测试代码
  • 持续集成
  • 重构

重构与性能

先写出可调优的软件,然后调优它以求获得足够的速度

第三章 代码的坏味道

1. 神秘命名

  • 改变函数声明(124)
  • 变量改名(137)
  • 字段改名(244)

    2. 重复代码

  • 同一个类的两个函数含有相同的表达式 —— 提炼函数(106)

  • 重复代码只是相似而不是完全相同 —— 移动语句(233) 重组代码顺序,把相似的部分放在一起以便提炼
  • 重复的代码位于一个超类的不同子类中 —— 函数上移(350) 避免在两个子类之间互相调用

    3. 过长函数

    我们遵循这样一条原则:每当感觉需要以注释来说明点什么的时候,我们就把需要说明的东西写进一个独立函数中,并以其用途(而非实现手法)命名。 哪怕替换后的函数调用比函数本身还长,只要函数名称能够解释其用途,我们也该毫不犹豫的这么做,关键不在于函数的长度,而在于函数“做什么”和“如何做”之间的语义距离。

  • 提炼函数(106) —— 找到函数中适合集中在一起的部分,将他们提炼出来形成一个新函数

  • 函数内有大量的参数和临时变量,对函数提炼造成阻碍 —— 查询取代临时变量(178) 消除临时元素, 引入参数对象(140) 保持对象完整(319) 将过长的参数列表变得更简洁一些。
  • 杀手锏:以命令取代函数(337)
  • 如何确定该提炼哪一段代码呢?寻找注释。如果代码前方有一行注释,就是在提醒你可以把这段代码替换成一个函数,而且可以在注释的基础上给这个函数命名。就算只有一行代码,也可以这么做

    4.过长参数列表

    • 如果可以向某个参数发起查询而获得另一个参数的值 —— 以查询取代参数(324) 去掉第二个参数
    • 从现有的数据结构中抽出很多数据项 —— 保持对象完整(319) 直接传入原本的数据结构
    • 有几项参数总是同时出现 —— 引入参数对象(140) 将其合并成一个对象
    • 某个参数被用作区分函数行为的标记(flag) —— 移除标记参数(314)
    • 多个函数有同样的几个参数 —— 函数组合成类(144) 将这些共同的参数变成这个类的字段

      5. 全局数据

  • 封装变量(132) ,并最好将这个函数以及其封装的数据搬移到一个类或模块中,只允许模块内的代码使用它,从而尽量控制其作用域

    6. 可变数据

  • 封装变量(132) —— 确保所有数据更新操作都通过很少几个函数来进行,使其更容易监控和演进

  • 一个变量在不同时候被用于存储不同的东西 —— 拆分变量(240) 将其拆分为各自不同用途的变量,从而避免危险的更新操作
  • 移动语句(223) 和 提炼函数(106) —— 尽量把逻辑从处理更新操作的代码中搬出来,将没有副作用的代码与执行数据更新操作的代码分开

    7. 发散式变化

  • 含义:某个模块经常因为不同的原因在不同的方向上发生变化

  • 每当对某个上下文做修改时,我们只需要理解这个上下文,不必操心另一个,也就是「每次只关心一个上下文」,这非常重要。
  • 发生变化的两个方向自然的形成了先后次序 —— 拆分阶段(154) 分开两者,用一个清晰的数据结构进行沟通
  • 如果两个方向之间有更多的来回调用,就应该先创建适当的模块,然后 —— 搬移函数(198)把处理逻辑分开
  • 如果函数内部混合了两类处理逻辑 —— 提炼函数(106)将其分开,然后做搬移
  • 如果模块以类的形式定义的—— 提炼类(182)来做拆分

    8. 霰弹式修改

  • 含义:当遇到某种变化,你需要在许多不同的类内做出许多小修改。

  • 搬移函数(198) 搬移字段(207) —— 把所有需要修改的代码放进同一个模块里
  • 如果很多代码都在操作相似的数据 —— 函数组合成类(144)
  • 如果函数的共同功能是 转化 或 充实数据结构 —— 函数组合成变换
  • 如果函数的输出可以组合成一段专门使用这些计算结果的逻辑 —— 拆分阶段(154)
  • 内联函数(115) 或 内联类(186) —— 将本不该分散的函数拽回一处

    9. 依恋情结

  • 含义: 一个函数与另一个模块中的函数或者数据交流格外频繁,远胜于在自己所处模块内部的交流

  • 如果一个函数用到了几个模块的功能,判断原则是:判断哪个模块拥有的此函数使用的数据最多,就把这个函数和那些数据摆在一起

    10. 数据泥团

  • 含义:两个类中相同的字段,许多函数签名中相同的参数

  • 判断依据:删掉众多数据中的一项,如果这么做,其他数据如果失去了意义,这就是个 「为这些在一起出现的数据新建属于他们自己的对象」的明确信号。

    11. 基本类型偏执

  • 典型案例:用字符串表示电话号码,这种变量甚至被称为“类字符串类型”变量

  • 对象取代基本类型(174)
  • 如果想要替换的数据值是控制条件行为的类型码 —— 以子类取代类型码(362) 以多态取代条件表达式(272) 组合替换

    12. 重复的switch

  • 多态取代条件表达式(272)

    13. 循环语句

  • 以管道取代循环(231)

    14. 冗赘的元素

  • 内联函数(115) 或 内联类(186)

  • 如果这个类处于一个继承体系中 —— 折叠继承体系(380)

    15. 夸夸其谈通用性

  • 典型案例:函数或类的唯一用户是测试用例

  • 如果某个抽象类其实没太大作用 —— 折叠继承体系(380)
  • 不必要的委托 —— 内联函数(115) 或 内联类(186)
  • 函数的某些参数未被用上 —— 改变函数声明(124)去掉这些参数
  • 有并非真正需要,只是为了不知远在何处的将来而塞进去的参数 —— 改变函数声明(124) 去掉

    16. 临时字段

  • 含义:某个类的内部有一个为某种特定情况而设置的字段

  • 提炼类(182) —— 给这个字段创建一个类,然后 —— 搬移函数(198) 把所有和这些字段相关的代码都放进这个新家

    17. 过长的消息链

  • 隐藏委托关系(189)

    • 先观察消息链最终得到的对象是用来干什么的,能否使用 —— 提炼函数(106) 将这个对象的代码提炼到一个独立的函数中
    • 再运用 —— 搬移函数(198) 把这个函数推入消息链

      18. 中间人

      19. 内幕交易

      20. 过大的类

      21. 异曲同工的类

      22. 纯数据类

      23. 被拒绝的遗赠

      24. 注释