《重构》阅读笔记
| 2023-5-18
0  |  阅读时长 0 分钟
日期
Apr 4, 2023 → Apr 13, 2023
Tags
笔记
方法论
记录阅读《重构》时的所思所想
💡—— 标记书中提及的要点

第1章

💡 如果你要给程序添加一个特性,但发现代码因缺乏良好的结构而不易于进行更改,那就先重构那个程序,使其比较容易添加该特性,然后再添加该特性。

重构的第一步

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

分解函数

  1. 提炼函数(106):将整个函数分离不同的关注点(关键语句,例如switch),并将它们抽取为独立的函数。
  1. 重构过程:小步修改,每次修改就运行测试 ⇒ 防止混乱
💡 重构技术就是以微小的步伐修改程序。如果犯下错误,很容易便可发现它
  1. 以查询取代临时变量(178):函数中的局部作用域的临时变量,也可以抽取出来,提炼为查询函数
  1. 内联变量(123):使用内联函数的方式简化语句,移除中间变量
  1. 改变函数声明(124):使用更为明确的函数名,减少可以简化的入参等
  1. 在提炼函数前,可以移除局部变量:
    1. 好处:简化提炼,减少需要考虑的局部作用域
  1. 临时变量会带来理解的难度,可以将其明确声明为函数
  1. 拆分循环(227):分离出循环语句
  1. 移动语句(223):将变量声明移动到紧邻循环的位置
  1. 对于重构过程的性能问题:大多数情况下可以忽略,若重构引入性能损耗,先完成重构,再做性能优化
  1. 重构工作:
    1. 早期:为原函数添加足够的结构,把复杂的代码块分解为更小的单元
  1. 实现复用:
    1. 拆分阶段(154):将代码的逻辑拆分成两个阶段,第一阶段创建中转数据结构,再把它传递给第二阶段
  1. 增加代码的模块化,可以提升代码可读性,增加复用,避免逻辑的混杂,虽然额外的包装会增加代码行数,但便于理解函数间的协作关系
编程时,需要遵循营地法则:保证你离开时的代码库一定比来时更健康
  1. 以多态取代条件表达式(272)
  1. 以子类型取代类型码(362)

第2章 重构的原则

  1. 重构:
      • 名词:对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本
      • 动词:使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构
  1. 重构的关键在于运用大量微小且保持软件行为的步骤
      • 重构过程中,代码很少进入不可工作的状态(确保重构的改变微小且不会影响代码使用)
      • 重构的目的:为了让代码“更容易理解,更易于修改”
        • 区别于性能优化:性能优化只关心如果让程序运行得更快,最终得到的代码可能更难理解和维护
  1. 两顶帽子:使用重构技术开发软件时,把时间分配给两种截然不同的行为
    1. 添加新功能:
      1. 不应该修改既有代码,只管添加新功能
      2. 通过添加测试并让测试正常运行
    2. 重构:
      1. 只管调整代码结构,不能再添加功能
      2. 不应该添加任何测试,只在绝对必要时才修改测试
      明白不同帽子对编程状态提出的不同要求
  1. 为何重构:
    1. 改进软件的设计:
        • 重构有助于防止程序内部设计(架构)的腐败变质,维持代码应有的形态
        • 消除重复代码,便于代码的未来修改和维护
    2. 使软件更容易理解
    3. 帮助找到bug:重构能够帮助写出更健壮的代码
    4. 提高编程速度:质量好的代码,更容易找到在哪里修改、如何修改,代码清晰,引入bug的可能性会变小,且代码的调试也更为简单
      1. notion image
        • 设计耐久性假说:通过投入精力改善内部设计,增加了软件的耐久性,从而可以更长时间地保持开发的快速。
  1. 何时重构
    1. 💡 三次法则:第一次做某件事时只管去做;第二次做类似的时会产生反感,但无论如何还是可以去做;第三次再做类似的事,你就应该重构。—— Don Roberts
      • 预备性重构:让添加新功能更容易
        • 重构的最佳时机在添加新功能前
        • 修改bug的时候也一样
      • 帮助理解的重构:使代码更易懂
        • 重构可以引领我们对获得对代码更高层面的理解
      • 捡垃圾式重构:及时清理代码的小问题(垃圾),积少成多
      • 有计划的重构和见机行事的重构
        • 见机行事的重构:上三种
          • 重构不是与编程割裂的行为
        • 有计划的重构:很少,大部分重构应该是不起眼的、见机行事的
      💡 肮脏的代码必须重构,但漂亮的代码也需要很多重构。
      💡 每次要修改时,首先令修改很容易(警告:这件事有时会很难),然后再进行这次容易的修改。
      • 长期重构:通常在需要大型重构时,例如需要替换一个正在使用的库、处理混乱的依赖关系、需要将整块代码抽取到一个组件中
      • 复审代码(code review)时重构:
        • 重构可以帮助代码复审工作的理解,提出建议
        • 推荐工作方式:与代码原作者坐在一起,一边浏览代码一边重构 ⇒ 自然地导向结对编程,在编程过程中持续不断地进行代码复审
  1. 重构的挑战
    1. 💡 重构的唯一目的就是让我们开发更快,用更少的工作量创造更大的价值
  1. 分支:
    1. 持续集成(Continuous Integration,CI),也叫基于主干开发(Trunk-Based Development):特性分支的生命应该较短,至少每天向主分支集成
    2. 特性开关(feature toggle/flag):用于隐藏尚未完成又无法拆小的功能
  1. 测试:
    1. 必要性:避免重构导致的错误
      1. 关键在于快速发现错误 ⇒ 代码需要完备的测试套件
  1. 三大实践:自测试代码、持续集成、重构
  1. 三种快速编写软件的方法:
    1. 时间预算法:预先分配不同组件的资源(时间、空间),并严格执行
    2. 持续关注法:要求任何程序员在任何时间做任何事情都要设法保持系统的高性能
    3. 利用度量工具统计程序运行数据:找出性能热点,对其进行改进,即“发现热点,去除热点”

第3章 代码的坏味道

  1. 神秘命名
    1. 代码中的命名需要清晰地表明其(函数、模块、变量和类)的功能和用法
    2. 改名是最常用的重构手法
      1. 改变函数声明(124)
      2. 变量改名(137)
      3. 字段改名(244)
  1. 重复代码
    1. 提炼函数(106):提炼出重复的代码
    2. 移动语句(223):若重复代码不是完全相同,可以通过移动语句重组代码顺序,再进行提炼
    3. 函数上移(350)
  1. 过长函数:
    1. 函数越长越难理解
        • 小函数的价值——间接性:更好的阐释力、更易于分享、更多的选择
    2. 原则:每当觉得需要以注释来说明点什么的时候,我们就把需要说明的东西写进一个独立函数中,并以其用途命名
    3. 简化方法:
        • 临时变量会影响函数提炼,可以运用以查询取代临时变量(178)来消除这些临时变量
        • 引入参数对象(140)和保持对象完整(319)可以将过长的参数列表变简洁
        • 以命令取代函数(337)
    4. 如何确定提炼哪一段代码
      1. 寻找注释:当代码需要使用注释进行解释时,可以将该代码提炼到独立函数中(尽管只有一行代码)
      2. 条件表达式:使用分解条件表达式(260)处理
        1. eg:对庞大的switch语句,应该对每个分支进行提炼函数(106)将其变为独立的函数调用
          eg:对于多个switch语句基于同一个条件进行分支选择,应使用以多态取代条件表达式(272)
      3. 循环:将循环和循环内的代码提炼到一个独立的函数中
          • 若提炼的循环难以命名,说明其做了几件不同的事,可以使用拆分循环(227)将其拆分成各自独立的任务
  1. 过长参数列表
    1. 以查询取代参数(324):代码向某个参数发起查询而获得另一个参数的值时,可以通过以查询取代的方式去掉第二个参数
    2. 保持对象完整(319):代码从现有的数据结构中抽出很多数据项,可以直接传入原来的数据结构
    3. 引入参数对象(140):几项参数总是同时出现,可以将其合并为一个对象
    4. 移除标记参数(314):某个参数被作为区分函数行为的标记
    5. 函数组合成类(144):多个函数有同样的几个参数
  1. 全局数据:
    1. 全局数据(全局变量、类变量和单例):问题在于可以在任意地方被修改
    2. 封装变量(132):将全局数据用函数包装起来,控制对其的访问,最好将该函数(及其封装的数据)搬移到一个类或模块中,只允许模块内的代码使用它,尽量控制其作用域。
  1. 可变数据:
    1. 数据永不改变的编程流派——函数式编程,如果要更新一个数据结构,就返回一份新的数据副本,旧的数据仍保持不变
    2. 封装变量(132):确保所有数据更新操作都通过很少几个函数进行,更容易监控和演进
    3. 拆分变量(240):当一个变量在不同时候用于存储不同的东西时,可以将其拆分为各自不同用途的变量,避免危险的更新操作
    4. 移动语句(223)和提炼函数(106):把逻辑从处理更新的操作中搬移,将没有副作用的代码与执行数据更新操作的代码分开
    5. 将查询函数和修改函数分离(306):设计API时,将查询函数和修改函数分离,确保调用者不会调到有副作用的代码
    6. 移除设值函数(331)
    7. 以查询取代派生变量(248):当可变数据的值能在其他地方计算出来时,以查询取代派生变量
    8. 函数组合成类(144)或函数组合成变量(149):限制需要对变量进行修改的代码量
    9. 将引用对象改为值对象(252):当变量的内部结构中包含了数据,最好不要直接修改其中的数据,而是用将引用对象改为值对象令其直接替换整个数据结构
  1. 发散式变化:发散式变化可以理解为,一个模块会因为不同的原因在不同的方向上发生变化。
    1. 拆分阶段(154):若发生变化的两个方向自然形成了先后顺序,可以通过拆分阶段将两者分开,两者之间通过一个清晰的数据结构进行沟通
    2. 搬移函数(198):若两个方向之间有更多的来回调用,应该先创建适当的模块,然后用搬移函数把处理逻辑分开
    3. 提炼函数(106):对于函数内部混合了两类处理逻辑,现用提炼函数将其分开,再做搬移
    4. 提炼类(182):若模块以类的形式定义,可以用提炼类来做拆分
  1. 霰弹式修改:如果每遇到某种变化,你都必须在许多不同的类内做出许多小修改,即霰弹式修改,需要修改的代码可能散布四处、难以找到,可能遗漏。
    1. 搬移函数(198)和搬移字段(207):把所有需要修改的代码放进同一个模块,
    2. 函数组合成类(144):当很多函数都在操作相似的数据
    3. 函数组合成变换(149):当函数的功能是转化或者充实数据结构
    4. 拆分阶段(154):当函数的输出可以组合后提供给一段专门使用这些计算结构的逻辑
    5. 内联函数(115)或内联类(186):霰弹式修改的常用策略,把不该分散的逻辑放在一起
  1. 依恋情结:指代码模块之间的数据交流频繁
    1. 提炼函数(106)+搬移函数(198):可以将函数和数据搬移到一起,若只有一部分函数与数据相关,可以将其抽取为独立的函数
    2. 原则:将总是一起变化的东西放在一块儿
  1. 数据泥团:
    1. 提炼类(182):将总是成群出现的相同字段提炼到独立对象中
    2. 引入参数对象(140)或保持对象完整(319):可以简短参数列表,简化函数调用
  1. 基本类型偏执:编程中大量使用基本类型
    1. 运用对象取代基本类型(174):将原本单独存在的数据值替换为对象(如字符串表示的电话号码、数值表示的英寸等物理量,这些情况简单使用基本类型会导致代码复杂和隐藏bug)
    2. 以子类取代类型码(362)+以多态取代条件表达式(272):想要替换的数据值是控制条件行为的类型码时
    3. 提炼类(182)+引入参数对象(140):有总是同时出现的基本数据类型
  1. 重复的switch:问题在于每当想要增加一个选择分支时,必须找到所有的switch,并逐一更新。
    1. 以多态取代条件表达式(272)
  1. 循环语句
    1. 以管道取代循环(231):管道操作(filter和map)可以帮助我们更快地看清被处理的元素以及处理它们的动作
  1. 冗赘的元素:程序元素(如类或函数)能给代码增加结构,从而支持变化、促进复用,但有时候并不需要这层额外的结构
    1. 使用内联函数(115)或内联类(186):对于简单函数或简单类
    2. 折叠继承体系(380):类出于继承体系中时
  1. 夸夸其谈通用性:无用的通用性代码,如果用不到就不值得
    1. 折叠继承体系(380):没什么作用的抽象类
    2. 内联函数(115)和内联类(186):不必要的委托
    3. 改变函数声明(124):用不上的函数参数
    4. 移除死代码(237):函数或类的唯一用户是测试用例,删掉测试用例,然后移除死代码
  1. 临时字段:
    1. 提炼类(182):某个字段仅为某种特殊情况而设时,为其创建一个类
    2. 搬移函数(192):将所有与字段相关的代码放入新建的类中
    3. 引入特例(289):在“变量不合法”的情况下创建替代对象,补面写出条件式代码
  1. 过长的消息链:
    1. 消息链:用户向一个对象请求另一个对象,然后再向后者请求另一个对象,然后再请求另一个对象……
    2. 隐藏委托关系(189):在消息链的不同位置采用该方法
    3. 提炼函数(106)+搬移函数(198):观察消息链最终得到的对象,看是否可以将该对象的代码提炼到独立函数中,再使用搬移函数将该函数推入消息链中
  1. 中间人:过度运用委托,例如某个类的接口有一半函数都委托给其他类
    1. 移除中间人(192):当出现过度运用委托时,可以移除中间人,让类直接和真正负责的对象打交道
    2. 内联函数(115):若使用委托的函数只有少数,可以使用内联函数将它们放进调用端
    3. 以委托取代超类(399)或以委托取代子类(381):当中间人还有其他行为时,可以通过这两种方法把它变成真正的对象,既扩展原对象行为,又不必负担那么多的委托动作
  1. 内幕交易:对于模块之间的数据交换,应该尽量减少这种行为,并它们都放到明面上
    1. 搬移函数(198)和搬移字段(207):减少模块的私下交流
    2. 隐藏委托关系(189):若两个模块有共同的兴趣,可以尝试新建模块,把共用数据放在一个管理良好的地方,或使用隐藏委托关系,把另一个模块变成两者的中介
    3. 以委托取代子类(381)或以委托取代超类(399):继承导致的数据交换,可以用上述两种方式,让它离开继承体系
  1. 过大的类:单个类中的函数太多
    1. 提炼类(182)
    2. 提炼超类(375)或以子类型取代类型码(362):组件适合作为子类
    3. 提炼类(182)、提炼超类(375)或以子类型取代类型码(362):观察使用者是否用到了这个类所有功能的子集,尽可能将每个子集拆分成独立的类
  1. 异曲同工的类:
    1. 改变函数声明(124)
    2. 搬移函数(198)
    3. 提炼超类(375)
  1. 纯数据类:指拥有一些字段以及仅包含访问这些字段函数的类,这些类数据操作行为一定会被其他类操作,因此要把处理数据的行为搬移到纯数据类中
    1. 封装记录(621):封装public字段
    2. 移除设值函数(331):针对不该被其他类修改的字段
    3. 搬移函数(198):将取值/设值函数被其他类调用的地方都搬移到纯数据类中
    4. 提炼函数(106):当整个函数无法直接搬移时
  1. 被拒绝的遗赠:子类仅使用了从超类中基层的部分函数或数据
    1. 函数下移(359)和字段下移(361):为子类新建兄弟类,把所有用不到的函数下推给那个兄弟,使得超类只持有所有子类共享的东西
    2. 以委托取代子类(381)或以委托取代超类(399):子类复用了超类的行为,但是不支持超类的接口,拒绝继承超类的实现时,没有必要使用继承
  1. 注释:注释可以帮助找到代码的坏味道,重构之后可以去除多余的注释(代码可以清晰说明)
    1. 提炼函数(106)
    2. 改变函数声明(124)
    3. 引入断言(302)

第4章 构筑测试体系

  1. 自测试代码的价值
    1. 自测试代码可以更频繁的运行
    2. 💡 确保所有测试都完全自动化,让它们检查自己的测试结果
    3. 每次编译时都运行测试,能够及时发现bug,且由于bug必定是最近修改代码时引入的,代码量少,能够更快定位bug
    4. 💡 一套测试就是一个强大的bug侦测器,能够大大缩减查找bug所需的时间
      • 测试驱动开发(Test-Driven Development,TDD):
        • 依赖短循环“测试、编码、重构”
  1. 撰写测试
    1. JavaScript中的Mocha框架
    2. 简单教程
      总是确保测试不该通过时真的会失败
      频繁地运行测试。对于你正在处理的代码,与其对应的测试至少每隔几分钟就要运行一次,每天至少运行一次所有的测试
      • 测试夹具(fixture):测试所需要的数据和对象
  1. 测试是风险驱动的行为,测试的目标是希望找出现在或未来可能出现的bug,过于简单的代码不需要测试
    1. 💡 编写未臻完善的测试并经常运行,好过对完美测试的无尽等待
  1. 确保测试的独立性,尽量不要使用公共变量,测试中隐含的对共享对象的修改可能会导致测试失败,造成测试结果的不确定性
    1. ⇒ 重复代码可以放到befroeEach子句(Mocha框架)中,该子句会在每个测试前运行一次,确保测试的独立性,避免了可能带来麻烦的不确定性
      ⇒ 当可以确保测试夹具百分百不可变时,也可以共享它
  1. 探测边界条件:可以帮助思考边界情况的合理性,完善代码逻辑
    1. 重构应保证可观测的行为不发生改变
    2. 测试应该集中在可能出错的地方
    3. 💡 不要因为测试无法捕捉所有的bug就不写测试,因为测试的确可以捕捉到大多数bug

第5章 介绍重构的名录

第6章 第一组重构

最有用的一组重构
  1. 提炼函数(Extract Function)(106)
      • 反向重构:内联函数(115)
  1. 内联函数(Inline Function)(115)
      • 反向重构:提炼函数(106)
      • 去除非必要的间接性
  1. 提炼变量(Extract Variable)(119)
      • 反向重构:内联变量(123)
  1. 内联变量(Inline Variable)(123)
      • 反向重构:提炼变量(119)
  1. 改变函数声明(Change Function Declaration)
  1. 封装变量(Encapsulate Variable)
  1. 变量改名(Rename Variable)
  1. 引入参数对象(Introduce Parameter Object)
  1. 函数组合成类(Combine Functions into Class)(144)
  1. 函数组合成变换(Combine Functions into Transform)
      • 函数组合成类和函数组合成变换可以相互替换,两者的重要区别为:
        • 若代码需要对源数据进行更新,使用类更好
  1. 拆分阶段

第7章 封装

  1. 封装记录(Encapsulate Record)
    1. 使用类包裹记录变量,并定义访问函数
  1. 封装集合(Encapsulate Collection)
      • 为类提供修改集合的方法(添加+移除)
      • 不要让集合的取指函数返回原始集合,如果可以请始终返回副本集合
  1. 以对象取代基本类型(Replace Primitive with Object)
  1. 以查询取代临时变量(Replace Temp with Query)
  1. 提炼类(Extract Class)(182)
      • 反向重构:内联类(186)
  1. 内联类(Inline Class)(186)
      • 反向重构:提炼类(182)
  1. 隐藏委托关系(Hide Delegate)(189)
      • 反向重构:移除中间人(192)
      • 对于每个委托关系中的函数在服务对象端建立一个简单的委托函数,将委托关系隐藏起来,去除对于委托的依赖
  1. 移除中间人(Remove Middle Man)(192)
      • 反向重构:隐藏委托关系(189)
  1. 替换算法(Substitute Algorithm)

第8章 搬移特性

  1. 搬移函数(Move Function)
  1. 搬移字段(Move Field)
  1. 搬移语句到函数(Move Statements into Function)(213)
      • 反向重构:搬移语句到调用者(127)
  1. 搬移语句到调用者(Move Statements to Callers)(127)
      • 反向重构:搬移语句到函数
  1. 以函数调用取代内联代码(Replace Inline Code with Function Call)
  1. 移动语句(Slide Statements)
  1. 拆分循环(Split Loop)
  1. 以管道取代循环(Replace Loop with Pipeline)
  1. 移除死代码(Remove Dead Code)

第9章 重新组织数据

  1. 拆分变量(Split Variable)
      • 每个变量应该只承担一个责任,当出现变量承担多个责任,它就应该被替换为多个变量
  1. 字段改名(Rename Field)
      • 对于广泛使用的数据解耦口,需要提前封装字段
  1. 以查询取代派生变量(Replace Derived Variable with Query)
  1. 将引用对象改为值对象(Change Reference to Value)(252)
      • 反向重构:将值对象改为引用对象(256)
  1. 将值对象改为引用对象(256)
      • 反向重构:将引用对象改为值对象(252)
      • 对于重复的对象(值对象),转为引用对象,可以避免数据修改导致的混乱和不一致性
        • 声明一个仓库对象,存储引用对象

第10章 简化条件逻辑

  1. 分解条件表达式(Decompose Conditional)
      • 对条件判断和每个条件分支分别运用提炼函数(106)手法
  1. 合并条件表达式(Consolidate Conditional Expression)
  1. 以卫语句取代嵌套条件表达式(Replace Nested Conditional with Guard Clauses)
      • 卫语句(guard clauses):单独检查的条件语句
      • 精髓:给以某分支特别的重视
  1. 以多态取代条件表达式(Replace Conditional with Polymorphism)
  1. 引入特例(Introduce Special Case)
      • 用特例值替换为代表这种特例情况的类或数据结构
  1. 引入断言(Introduce Assertion)
      • 使用断言明确表明代码中的假设(举例:代码假设某个条件始终为真)
      • 断言对于发现错误源头特有帮助,用于预测错误

第11章 重构API

  1. 将查询函数和修改函数分离(Separate Query from Modifier)
      • 原则:任何有返回值的函数都不应该有看得到的副作用——命令与查询分离
        • 当遇到“既有返回值又有副作用”的函数,将查询动作从修改动作中分离出来
  1. 函数参数化(Parameterize Function)
      • 抽离出相似的重复函数,改用一个同一函数,通过传参数的方式控制函数的内容
  1. 移除标记参数(Remove Flag Argument)
      • 标记参数的缺点:增加了函数的难理解性,隐藏了函数调用的差异性
  1. 保持对象完整(Preserve Whole Object)
      • 传递整个记录的方式能更好地应对变化(需要从记录中导出更多的数据时,不需要修改参数列表)
  1. 以查询取代参数(Replace Parameter with Query)(324)
      • 反向重构:以参数取代查询(237)
  1. 以参数取代查询(Replace Query with Parameter)(237)
      • 反向重构:以查询取代参数(324)
  1. 移除设值函数(Remove Setting Method)
      • 对于不可变的数据,可以通过移除设值函数更清晰地表达设计意图(避免潜在错误)
  1. 以工厂函数取代构造函数(Replace Constructor with Factory Function)
  1. 以命令取代函数(Replace Function with Command)(337)
      • 反向重构:以函数取代命令(344)
      • 命令:在此处指对象,其中封装了函数调用请求
      • 命令对象提供了比普通函数更大的控制灵活性和更强的表达能力
  1. 以函数取代命令(Replace Command with Function)(344)
      • 反向重构:以命令取代函数(337)

第12章 处理继承关系

  1. 函数上移(Pull Up Method)(350)
      • 反向重构:函数下移(359)
  1. 字段上移(Pull Up Field)(353)
      • 反向重构:字段下移(361)
  1. 构造函数本体上移(Pull Up Constructor Body)
  1. 函数下移(Push Down Method)(359)
      • 反向重构:函数上移(350)
  1. 字段下移(Push Down Field)(361)
      • 反向重构:字段上移(353)
  1. 以子类取代类型码(Replace Type Code with Subclasses)(362)
      • 反向重构:移除子类(369)
  1. 移除子类(Remove Subclass)(369)
      • 反向重构:以子类取代类型码(362)
  1. 提炼超类(Extract Superclass)
  1. 折叠继承体系(Collapse Hierarchy)
  1. 以委托取代子类(Replace Subclass with Delegate)
  1. 以委托取代超类(Replace Superclass with Delegate)
      • 当超类中的一些函数对子类并不适用时
Loading...
目录