《重构》1-6章读书笔记

发布时间 2023-05-31 11:48:21作者: Xianhao

《重构》1-6章读书笔记

重构的定义

所谓重构(refactoring)是这样一个过程:在不改变代码外在行为的前提下,对代码做出修改,以改进程序的内部结构。重构是一种经千锤百炼形成的有条不紊的程序整理方法,可以最大限度地减小整理过程中引入错误的概率。

本质上说,重构就是在代码写好之后改进它的设计。

为什么需要进行重构

  1. 需求变化

    需求的变化使重构变得必要。如果一段代码能正常工作,并且不会再被修改,那么完全可以不去重构它。能改进之当然很好,但若没人需要去理解它,它就不会真正妨碍什么。

  2. 新需求(预备性重构)
    重构的最佳时机在添加新功能之前,在动手添加新功能之前,我会看看现有的代码库,此时经常会发现:如果对代码结构做一点微调,工作会变的容易很多(旧代码重构来扩展新功能)

  3. 帮助理解的重构
    我需要先理解代码在做什么,然后才能着手修改。这段代码可能是我写的,也可能是别人写的。一旦我需要思考“这段代码到底在做什么”,我就会自问:能不能重构这段代码,令其一目了然?我可能看见了一段结构糟糕的条件逻辑,也可能希望复用一个函数,但花费了几分钟才弄懂它到底在做什么,因为它的函数命名实在是太糟糕了。这些都是重构的机会。

  4. 捡垃圾式重构
    当我在重构过程中或者开发过程中,发现某一块不好,如果很容易修改可以顺手修改,但如果很麻烦,我又有紧急事情时候,可以选择记录下来(但不代表我就一点都做不到把他变好)。就像野营者的老话:至少让营地比你到达时更干净,久而久之,营地就非常干净(来自营地法则)

  5. 见机行事的重构
    重构经常发生在我们日常开发中,随手可改的地方。当我们发现不好的味道,就要将他重构

  6. 长期的重构
    可以在一个团队内,达成共识。当大家遇到时候,就改正它例如,如果想替换一个正在使用的库,可以先引入一层新的抽象,使其兼容新旧两个库的接口,然后一旦调用方完全改为了使用这层抽象,替换下面的库就会如容易的多

  7. 复审代码(code review)时的重构
    开发者与审查者保持持续沟通,使得审查者能够深入了解逻辑,使得开发者能充分认同复审者的修改意见(结对编程)

我何时该进行重构

书中的原话:第一次做某件事时只管去做;第二次做类似的事会产生反感,但无论如何还是可以去做;第三次再做类似的事,你就应该重构正如老话说的:事不过三,三则重构

重构的意义

  1. 改进软件的设计(也可以说是增加程序的健壮、耐久)
  2. 使得代码更容易理解
  3. 找到潜在bug
  4. 提高编程速度

重构的挑战

  1. 延缓新功能开发
    实际上,这只是一部分不理解重构真正原因的人的想法,重构是为了从长效上见到收益,一段优秀的代码能让我们开发起来更顺手,要权衡好重构与新功能的时机,比如一段很少使用的代码。就没必要对他重构
  2. 代码所有权
    有时候我们经常会遇到,接口发布者与调用者不是同一个人,并且甚至可能是用户与我们团队的区别,在这种情况下,需要使用函数改名手法,重构新函数,并且保留旧的对外接口来调用新函数,并且标记为不推荐使用。
  3. 分支的差异
    经常会有长期不合并的分支,一旦存在时间过长,合并的可能性就越低,尤其是在重构时候,我们经常要对一些东西进行改名和变化,所以最好还是尽可能短的进行合并,这就要求我们尽可能的将功能颗粒化,如果遇到还没开发完成且又无法细化的功能,我们可以使用特性开关对其隐藏
  4. 缺乏一组自测试的代码
    一组好的测试代码对重构很有意义,它能让我们快速发现错误,虽然实现比较复杂,但他很有意义
  5. 遗留代码
    不可避免,一组别人的代码使得我们很烦恼,如果是一套没有合理测试的代码则使得我们更加苦恼。这种情况下,我们需要增加测试,可以运用重构手法找到程序的接缝,再接缝处增加测试,虽然这可能有风险,但这是前进所必须要冒的风险,同时不建议一鼓作气的把整个都改完,更倾向于能够逐步地推进

代码的坏味道

碰到什么样的代码时需要进行重构

神秘命名

描述:包含那些随意的abcd或者汉语拼音,总之一切我们看不懂的、烂的都算,好的命名能节省我们很大的时间

如何重构:改变函数声明、变量改名、字段改名

重复代码

描述:只要是我们看到两段相似的语法都可以确定为这段代码可以提炼

如何重构:提炼函数、移动语句、函数上移等手法

过长的函数

描述:比如一些条件分支、一个函数做了很多事情、循环内的处理等等的都是应该重构的

如何重构:提炼函数(常用)、以查询取代临时变量、引入参数对象、保持对象完整性、以命令取代参数(消除一些参数)、分解条件表达式、以多态取代条件表达式(应对分支语句)、拆分循环(应对一个循环做了很多事情)

过长的参数列表

描述:正常来说,函数中所需的东西应该以参数形式传入,避免全局变量的使用,但过长的参数列表其实也很恶心。
如何重构:查询取代参数、保持对象完整、引入参数对象、移除标记参数、函数组合成类

全局数据

描述:最常见的就是全局变量,但类变量与单例模式也有这样的问题,我们通常无法保证项目启动后不被修改,这就很容易造成诡异的bug,并且很难追查到
重构:封装变量

可变数据

描述:数据的可变性和全局变量一样,如果我其他使用者修改了这个值,而引发不可理喻的bug。 这是很难排查的。
重构:封装变量,拆分变量,移动语句、提炼函数,查询函数和修改函数分离,移除设值函数,以查询取代变量函数组合成类

发散式变化

描述:发散式变化是指某个模块经常因为不同的原因在不同的方向上变化了(可以理解为某一处修改了,造成其他模块方向错乱)
重构:拆分阶段、搬移函数、提炼函数、提炼类

霰弹式修改

描述:和发散式变化接近,却又相反。我们每次修改一个功能或者新增一个功能都需要对多处进行修改;并且随着功能增多我们可能还需要修改更多。 这样程序时是很不健康的
重构:搬移函数、搬移字段、函数组合成类、函数组合成变换、拆分阶段、内联函数、内联字段

依恋情结

描述:一个模块内的一部分频繁的和外面的模块进行交互沟通,甚至超过了它与内部的沟通。也就是违反了高内聚低耦合,遇到这种的“叛乱者”,不如就让他去他想去的地方吧
重构:搬移函数、提炼函数

数据泥团

描述:杂合缠绕在一起的。代码中也如是,我们可能经常看到三四个相同的数据,两个类中相同字段等等。总之像泥一样,这里也是这样那里也是这样,就是他了
重构:提炼类、引入参数对象、保持对象完整性

基本类型偏执

描述:一些基本类型无法表示一个数据的真实意义,例如电话号码、温度等,
重构:以对象取代基本类型、以子类取代类型码、以多态取代条件表达式

重复的switch

描述:不只是switch,大片相联的if也应该包含在内。
重构:多态取代条件表达式

循环语句

描述:传统的for类循环
重构:用map、forEach、reduce、filter来取代循环

冗赘的元素

描述:元素指类和函数,但是这些元素可能因为种种原因,导致函数过于小,导致没有什么作用,以及那些重复的,都可以算作冗赘
重构:内联函数、内联类、折叠继承类

夸夸其谈通用性

描述:为了将来某种需求而实现的某些特殊的处理,但其实可能导致程序难以维护难以理解,直白来说就是没个锤子用的玩意,你留下他干个屁
重构:折叠继承体系、内联函数、内联类、改变函数声明、移除死代码

临时字段

描述:那些本身就足以说明自己是谁的,不需要名字来描述的
重构:提炼类、提炼函数、引入特例

过长的消息链

描述:一个对象请求另一个对象,然后再向后者请求另一个对象,然后再请求另一个对象……这就是消息链,举个例子来说 new Car().properties.bodyWork.material.composition().start() 这意味着在查找过程中,很多的类耦合在一起。个人认为,不仅是结构的耦合,也很难理解。这也包含某类人jq的那一大串的连续调用。都是很难让人理解的。
重构: 隐藏委托关系、提炼函数、搬移函数

中间人

描述:如果一个类有大部分的接口(函数)委托给了同一个调用类。当过度运用这种封装就是一种代码的坏味道
重构:移除中间人、内联函数

内幕交易

描述:两个模块的数据频繁的私下交换数据(可以理解为在程序的不为人知的角落),这样会导致两个模块耦合严重,并且数据交换隐藏在内部,不易被察觉
重构:搬移函数、隐藏委托关系、委托取代子类、委托取代超类

过大的类

描述:单个类做了过多的事情,其内部往往会出现太多字段,一旦如此,重复代码也就接踵而至。这也意味着这个类绝不只是在为一个行为负责
重构:提炼超类、以子类取代类型码

异曲同工的类

描述:两个可以相互替换的类,只有当接口一致才可能被替换
重构:改变函数声明、搬移函数、提炼超类

纯数据类

描述:拥有一些字段以及用于读写的函数,除此之外一无是处的类,一般这样的类往往半一定被其他类频繁的调用(如果是不可修改字段的类,不在此列,不可修改的字段无需封装,直接通过字段取值即可),这样的类往往是我们没有把调用的行为封装进来,将行为封装进来这种情况就能得到很大改善。
重构:封装记录、移除取值函数、搬移函数、提炼函数、拆分阶段

被拒绝的遗赠

描述:这种味道比较奇怪,说的是继承中,子类不想或不需要继承某一些接口,我们可以用函数下移或者字段下移来解决,但不值得每次都这么做,只有当子类复用了超类的行为却又不愿意支持超类的接口时候我们才应该做出重构
重构:委托取代子类、委托取代超类

注释

描述:这里提到注释并非是说注释是一种坏味道,只是有一些人经常将注释当作“除臭剂”来使用(一段很长的代码+一个很长的注释,来帮助解释)。往往遇到这种情况,就意味着:我们需要重构了
重构:提炼函数、改变函数声明、引入断言