快捷搜索:  汽车  科技

重构改善既有代码的设计:读书笔记-重构

重构改善既有代码的设计:读书笔记-重构本文章中主要分成三部分进行描述,第一部分为名字就是它的术语,第二部分为详解:它的描述及一些实际场景,第三部分重构:就是他的参考重构手法,但这些手法仅作为参考,有时我们可能会需要更多的手法本书之中的核心之一:简单来说就是碰到什么样子的代码,你就需要警惕起来,需要进行重构了!注: 本书经典语句比较多,只挑选了其中我觉得感悟最深刻的五点写出来Kent Beck提出了“两顶帽子”的比喻。使用重构技术开发软件时,我把自己的时间分配给两种截然不同的行为:添加新功能和重构。添加新功能时,我不应该修改既有代码,只管添加新功能。通过添加测试并让测试正常运行,我可以衡量自己的工作进度。重构时我就不能再添加功能,只管调整代码的结构。此时我不应该添加任何测试(除非发现有先前遗漏的东西),只在绝对必要(用以处理接口变化)时才修改测试软件开发过程中,我可能会发现自己经常变换帽子。首先我会尝试添加新功能,然后会意识到:

重构改善既有代码的设计:读书笔记-重构(1)

偶然发现重构这本书推出了js版,果断入手,名书之一,尤其还是js版本,相较于java版来说,肯定更适合前端阅读,购买来自当当。

本书作者 马丁·福勒,主要著作有:分析模式---可重用对象模型、Kent Beck. 规划极限编程、 UML精粹---标准对象建模语言简明指南(第三版)、 企业应用架构模式以及本书。本书内容以各种代码的“坏味道”,来推进合适的重构手法,和第一版内容相比,有一些部分是更新了(那些被淘汰的代码、不合适的例子)。但主体思想还是没有变,总而言之是一本值得读的好书

下面来分享本文章核心内容,读书笔记。

读书中感悟最深的名言
  1. 原文:重构技术就是以微小的步伐修改程序。如果你犯下错误,很容易便可发现它。 感悟:细碎的步子前进,可以使得我们避免bug 在实际中,我们应当是以稍大的步骤来改进,当遇到问题时,撤销我们的更改 转而走向细碎的步子推进
  2. 原文:重构前,先检查自己是否拥有一套可靠的测试集,这些测试必须有自我检视能力。 感悟:和鲍勃大叔在代码整洁之道(clean code)中的观点一致,先编写测试,才能再开发。重构亦如此
  3. 原文:一些重构手法也会显著地影响性能。但即便如此,我通常也不去管它,继续重构,因为有了一份结构良好的代码,回头调优其性能也容易得多 感悟:不要因为性能问题而不敢重构,一份好的代码再去调优是很容易的,更何况在现在各种缓存、压缩、浏览器的优化等加持下,真正影响性能的往往只是我们项目中的某一小块代码
  4. 原文:每当感觉需要以注释来说明点什么的时候,我们就把需要说明的东西写进一个独立函数中,并以其用途(而非实现手法)命名。我们可以对一组甚至短短一行代码做这件事。哪怕替换后的函数调用动作比函数自身还长,只要函数名称能够解释其用途,我们也该毫不犹豫地那么做。关键不在于函数的长度,而在于函数“做什么”和“如何做”之间的语义距离。 感悟:一个最好的程序就是不需要任何注释,自己本身就已经说明了程序的运转流程,这和在初入行时,老师让我们多写注释,最好每一行都写的不太一样,但也不算老师或者马丁大叔说错了,初入行时,我们什么都不懂,经常编写匪夷所思的代码,所以注释很必要,但是更好的注释是我们自身的代码。这个在很多代码规范中或者一些优秀的代码也都是这么做的。

tips:好的代码中,注释应该是短小精悍的,应该只是我们需要描述一些非常规做法的说明,或者隐患的说明以及我们某些时候冒出的一些对程序有用但没有做的想法

  1. 原文:事实上,撰写测试代码的最好时机是在开始动手编码之前。当我需要添加特性时,我会先编写相应的测试代码。听起来离经叛道,其实不然。编写测试代码其实就是在问自己:为了添加这个功能,我需要实现些什么?编写测试代码还能帮我把注意力集中于接口而非实现(这永远是一件好事)。预先写好的测试代码也为我的工作安上一个明确的结束标志:一旦测试代码正常运行,工作就可以结束了 感悟:编写测试代码的最佳时机是在开始编写之前,将业务最终效果变为测试代码,有利于我们明白我们为什么要做这个功能,需要实现什么样的东西。在作者看来,不论是新增功能、修改功能、bug fix都应该测试先行。这是一种正确的思路,也是我们现在很多程序员所缺少的。大部分人做的都是先写功能,再写测试。甚至一些公司连测试代码都没有

注: 本书经典语句比较多,只挑选了其中我觉得感悟最深刻的五点写出来

两顶帽子的概念

Kent Beck提出了“两顶帽子”的比喻。使用重构技术开发软件时,我把自己的时间分配给两种截然不同的行为:添加新功能和重构。添加新功能时,我不应该修改既有代码,只管添加新功能。通过添加测试并让测试正常运行,我可以衡量自己的工作进度。重构时我就不能再添加功能,只管调整代码的结构。此时我不应该添加任何测试(除非发现有先前遗漏的东西),只在绝对必要(用以处理接口变化)时才修改测试软件开发过程中,我可能会发现自己经常变换帽子。首先我会尝试添加新功能,然后会意识到:如果把程序结构改一下,功能的添加会容易得多,于是我换一顶帽子,做一会儿重构工作。程序结构调整好后,我又换上原先的帽子,继续添加新功能。新功能正常工作后,我又发现自己的编码造成程序难以理解,于是又换上重构帽子……整个过程或许只花10分钟,但无论何时我都清楚自己戴的是哪一顶帽子,并且明白不同的帽子对编程状态提出的不同要求。

  • 时刻牢记自己正在做什么,不要混在一起。这也是我自己所欠缺的东西
读书内容详解重构的定义
  • 所谓重构(refactoring)是这样一个过程:在不改变代码外在行为的前提下,对代码做出修改,以改进程序的内部结构。重构是一种经千锤百炼形成的有条不紊的程序整理方法,可以最大限度地减小整理过程中引入错误的概率。本质上说,重构就是在代码写好之后改进它的设计。
应该重构的原因
  1. 需求变化 需求的变化使重构变得必要。如果一段代码能正常工作,并且不会再被修改,那么完全可以不去重构它。能改进之当然很好,但若没人需要去理解它,它就不会真正妨碍什么。
  2. 新需求(预备性重构) 重构的最佳时机在添加新功能之前,在动手添加新功能之前,我会看看现有的代码库,此时经常会发现:如果对代码结构做一点微调,工作会变的容易很多(旧代码重构来扩展新功能)
  3. 帮助理解的重构 我需要先理解代码在做什么,然后才能着手修改。这段代码可能是我写的,也可能是别人写的。一旦我需要思考“这段代码到底在做什么”,我就会自问:能不能重构这段代码,令其一目了然?我可能看见了一段结构糟糕的条件逻辑,也可能希望复用一个函数,但花费了几分钟才弄懂它到底在做什么,因为它的函数命名实在是太糟糕了。这些都是重构的机会。
  4. 捡垃圾式重构 当我在重构过程中或者开发过程中,发现某一块不好,如果很容易修改可以顺手修改,但如果很麻烦,我又有紧急事情的时候,可以选择记录下来(但不代表我就一点都做不到把他变好)。就像野营者的老话:至少让营地比你到达时更干净,久而久之,营地就非常干净(来自营地法则)
  5. 见机行事的重构 重构经常发生在我们日常开发中,随手可改的地方。当我们发现不好的味道,就要将他重构
  6. 长期的重构 可以在一个团队内,达成共识。当大家遇到时候,就改正它例如,如果想替换一个正在使用的库,可以先引入一层新的抽象,使其兼容新旧两个库的接口,然后一旦调用方完全改为了使用这层抽象,替换下面的库就会如容易的多
  7. 复审代码(code review)时的重构 开发者与审查者保持持续沟通,使得审查者能够深入了解逻辑,使得开发者能充分认同复审者的修改意见(结对编程)

不知道何时该重构,那就遵循三次法则(来自书中)

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

重构的意义
  1. 改进软件的设计(也可以说是增加程序的健壮、耐久) 通过投入精力改善内部设计 我们增加了软件的耐久性 从而可以更长时间地保持开发的快速
  2. 使得代码更容易理解
  3. 找到潜在bug
  4. 提高编程速度

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

何时不应该重构
  1. 不需要修改的代码
  2. 隐藏在一个API之下,只有当我需要理解其工作原理时,对其进行重构才有价值
  3. 重写比重构还容易

重构与其他的关系
  1. 开发:短期会耽误一定的开发事件,但从长期来看,重构使得新功能会更容易开发
  2. 性能:会影响部分性能,但在大多数的加持下,显得微不足道,并且重构有利于性能优化的点集中于某一处或者几处
  3. 架构:相辅相成
  4. 需求:需求推动重构前进

代码的坏味道

本书之中的核心之一:简单来说就是碰到什么样子的代码,你就需要警惕起来,需要进行重构了!

本文章中主要分成三部分进行描述,第一部分为名字就是它的术语,第二部分为详解:它的描述及一些实际场景,第三部分重构:就是他的参考重构手法,但这些手法仅作为参考,有时我们可能会需要更多的手法

  1. 神秘命名 详解:也包含那些随意的abc或者汉语拼音,总之一切我们看不懂的、烂的都算,好的命名能节省我们很大的时间 重构:改变函数声明、变量改名、字段改名
  2. 重复代码 详解:自然这个就好理解了,只要是我们看到两段相似的语法都可以确定为这段代码可以提炼,通常提炼出来会更好,当然这个要看具体情况,个人感觉真的遇到那种只有两处,且代码使用地方八杆子打不着,在代码稳定期间也不用浪费这个时间(这个时间不止体现在改动过程,也包括你可能因为改动导致的隐藏bug 尤其是系统核心模块,一旦出现问题只能马上回滚,不会给你时间去找问题 重构:提炼函数、移动语句、函数上移等手法
  3. 过长的函数 描述:短小才是精悍!比如一些条件分支、一个函数做了很多事情、循环内的处理等等的都是应该重构的 重构:提炼函数(常用)、以查询取代临时变量、引入参数对象、保持对象完整性、以命令取代参数(消除一些参数)、分解条件表达式、以多态取代条件表达式(应对分支语句)、拆分循环(应对一个循环做了很多事情)
  4. 过长的参数列表 描述:正常来说,函数中所需的东西应该以参数形式传入,避免全局变量的使用,但过长的参数列表其实也很恶心。 重构:查询取代参数、保持对象完整、引入参数对象、移除标记参数、函数组合成类
  5. 全局数据 描述:最常见的就是全局变量,但类变量与单例模式也有这样的问题,我们通常无法保证项目启动后不被修改,这就很容易造成诡异的bug,并且很难追查到 重构:封装变量
  6. 可变数据 描述:数据的可变性和全局变量一样,如果我其他使用者修改了这个值,而引发不可理喻的bug。 这是很难排查的。 重构:封装变量,拆分变量,移动语句、提炼函数,查询函数和修改函数分离,移除设值函数,以查询取代变量函数组合成类
  7. 发散式变化 描述:发散式变化是指某个模块经常因为不同的原因在不同的方向上变化了(可以理解为某一处修改了,造成其他模块方向错乱) 重构:拆分阶段、搬移函数、提炼函数、提炼类
  8. 霰弹式修改 描述:和发散式变化接近,却又相反。我们每次修改一个功能或者新增一个功能都需要对多处进行修改;并且随着功能增多我们可能还需要修改更多。 这样程序时是很不健康的,其实我个人理解为:霰弹用来描述发散式变化更好,想想霰弹是一个点发射出去变成很多。而本条应该用另一个词来描述更好,但我还想不到叫什么词。或许叫多路并进?仅限个人观点,每个人理解可能不一样,建议以作者为准 重构:搬移函数、搬移字段、函数组合成类、函数组合成变换、拆分阶段、内联函数、内联字段
  9. 依恋情结 描述:一个模块内的一部分频繁的和外面的模块进行交互沟通,甚至超过了它与内部的沟通。也就是违反了高内聚低耦合,遇到这种的“叛乱者”,不如就让他去他想去的地方吧 重构:搬移函数、提炼函数
  10. 数据泥团 描述:杂合缠绕在一起的。代码中也如是,我们可能经常看到三四个相同的数据,两个类中相同字段等等。总之像泥一样,这里也是这样那里也是这样,就是他了 重构:提炼类、引入参数对象、保持对象完整性
  11. 基本类型偏执 描述:一些基本类型无法表示一个数据的真实意义,例如电话号码、温度等, 重构:以对象取代基本类型、以子类取代类型码、以多态取代条件表达式
  12. 重复的switch 描述:不只是switch,大片相联的if也应该包含在内,甚至在古老的前端时代,曾经一度无条件反对这样的写法。 重构:多态取代条件表达式
  13. 循环语句 描述:在js中体现为传统的for类循环 重构:用管道来取代循环(管道:map、forEach、reduce、filter等一系列)
  14. 冗赘的元素 描述:元素指类和函数,但是这些元素可能因为种种原因,导致函数过于小,导致没有什么作用,以及那些重复的,都可以算作冗赘 重构:内联函数、内联类、折叠继承类
  15. 夸夸其谈通用性 描述:为了将来某种需求而实现的某些特殊的处理,但其实可能导致程序难以维护难以理解,直白来说就是没个锤子用的玩意,你留下他干个屁 重构:折叠继承体系、内联函数、内联类、改变函数声明、移除死代码
  16. 临时字段 描述:那些本身就足以说明自己是谁的,不需要名字来描述的 重构:提炼类、提炼函数、引入特例
  17. 过长的消息链 描述:一个对象请求另一个对象,然后再向后者请求另一个对象,然后再请求另一个对象……这就是消息链,举个例子来说 new Car().properties.bodyWork.material.composition().start() 这意味着在查找过程中,很多的类耦合在一起。个人认为,不仅是结构的耦合,也很难理解。这也包含某类人jq的那一大串的连续调用。都是很难让人理解的。 重构: 隐藏委托关系、提炼函数、搬移函数
  18. 中间人 描述:如果一个类有大部分的接口(函数)委托给了同一个调用类。当过度运用这种封装就是一种代码的坏味道 重构:移除中间人、内联函数
  19. 内幕交易 描述:两个模块的数据频繁的私下交换数据(可以理解为在程序的不为人知的角落),这样会导致两个模块耦合严重,并且数据交换隐藏在内部,不易被察觉 重构:搬移函数、隐藏委托关系、委托取代子类、委托取代超类
  20. 过大的类 描述:单个类做了过多的事情,其内部往往会出现太多字段,一旦如此,重复代码也就接踵而至。这也意味着这个类绝不只是在为一个行为负责 重构:提炼超类、以子类取代类型码
  21. 异曲同工的类 描述:两个可以相互替换的类,只有当接口一致才可能被替换 重构:改变函数声明、搬移函数、提炼超类
  22. 纯数据类 描述:拥有一些字段以及用于读写的函数,除此之外一无是处的类,一般这样的类往往半一定被其他类频繁的调用(如果是不可修改字段的类,不在此列,不可修改的字段无需封装,直接通过字段取值即可),这样的类往往是我们没有把调用的行为封装进来,将行为封装进来这种情况就能得到很大改善。 重构:封装记录、移除取值函数、搬移函数、提炼函数、拆分阶段
  23. 被拒绝的遗赠 描述:这种味道比较奇怪,说的是继承中,子类不想或不需要继承某一些接口,我们可以用函数下移或者字段下移来解决,但不值得每次都这么做,只有当子类复用了超类的行为却又不愿意支持超类的接口时候我们才应该做出重构 重构:委托取代子类、委托取代超类
  24. 注释 描述:这里提到注释并非是说注释是一种坏味道,只是有一些人经常将注释当作“除臭剂”来使用(一段很长的代码 一个很长的注释,来帮助解释)。往往遇到这种情况,就意味着:我们需要重构了 重构:提炼函数、改变函数声明、引入断言

重构手法介绍

如果说上面的味道是核心的话,那手法应该就是本书的重中之重。通常我们发现哪里味道不对之后,就要选择使用不同的手法进行重构。将他们变得味道好起来。

本文中每个手法通常包含三个模块:时机(遇到什么情况下使用)、做法(详细步骤的概括)、关键字(做法的缩影)

提炼函数
  • 时机:
  1. 当我们觉得一段大函数内某一部分代码在做的事情是同一件事,并且自成体系,不与其他掺杂时
  2. 当代码展示的意图和真正想做的事情不是同一件时候,如作者提到的例子。想要高亮,代码意思为反色,这样就不容易让人误解,印证了作者前面说的:当你需要写一行注释时候,就适合重构了
  • 做法:
  1. 一个以他要做什么事情来命名的函数
  2. 待提炼代码复制到这个函数
  3. 检查这个函数内的代码的作用域、变量
  4. 编译查看函数内有没有报错(js可以通过eslint协助)
  5. 替换源函数的被提炼代码替换为函数调用
  6. 测试
  7. 替换其他代码中是否有与被提炼的代码相同或相似之处
  • 关键字: 新函数、拷贝、检查、作用域/上下文、编译、替换、修改细节
内联函数
  • 时机:
  1. 函数内代码直观表达的意思与函数名字相同
  2. 有一堆杂乱无章的代码需要重构,可以先内联函数,再通过提炼函数合理重构
  3. 非多态性函数(函数属于一个类,而这个类被继承)
  • 做法:
  1. 检查多态性(如果该函数属于某个超类,并且它具有多态性,那么就无法内联)
  2. 找到所有调用点
  3. 将函数所有调用点替换为函数本体(非一次性替换,可以分批次替换、适应新家、测试)
  4. 删掉该函数的定义(也可能会不删除,比如我们放弃了有一些函数调用,因为重构为渐进式,非一次性)
  • 关键字: 检查多态、找调用并替换、删除定义
提炼变量
  • 时机:
  1. 一段又臭又长的表达式
  2. 在多处地方使用这个值(可能是当前函数、当前类乃至于更大的如全局作用域)
  • 做法:
  1. 确保要提炼的表达式,对其他地方没有影响
  2. 声明一个不可修改的变量,并用表达式作为该变量的值
  3. 用新变量取代原来的表达式
  4. 测试
  5. 交替使用3、4
  • 关键字:

副作用、不可修改的变量、赋值、替换

内联变量
  • 时机:
  1. 变量没有比当前表达式有什么更好的释义
  2. 变量妨碍了重构附近代码
  • 做法:
  1. 检查确认变量赋值的右侧表达式不对其他地方造成影响
  2. 确认是否为只读,如果没有声明只读,则要先让他只读,并测试
  3. 找到使用变量的地方,直接改为右侧表达式
  4. 测试
  5. 交替使用3、4
  • 关键字

副作用、只读、替换变量

改变函数声明

最好能把大的修改拆成小的步骤,所以如果你既想修改函数名,又想添加参数最好分成两步来做。 不论何时,如果遇到了麻烦,请撤销修改,并改用迁移式做法)

  • 时机:
  1. 函数名字不够贴切函数所做的事情
  2. 函数参数增加
  3. 函数参数减少
  4. 函数参数概念发生变化
  5. 函数因为某个参数导致的函数应用范围小(全局有很多类似的函数,在做着类似的事情)
  • 做法(适用于确定了函数或者参数只在有限的小范围内使用,并且仅仅改名)
  1. 先确定函数体内有没有使用这个参数(针对于参数)
  2. 确定函数调用者(针对于函数)
  3. 修改函数/参数的声明,使其达到我们想要的效果
  4. 找到所有的函数/参数声明的地方将其改名
  5. 找到所有函数/参数调用的地方将其替换
  • 关键字

使用变量者、函数调用者、修改函数、声明改名、调用替换

  • 做法(标准化做法)
  1. 对函数内部进行重构(如果有必要的话)
  2. 使用提炼函数手法,将函数体提炼成一个新函数,同名的话,可以改为一个暂时的易于搜索的随意名字(如:aaa_getData,只要好搜索且唯一即可。),非同名的话,使用我们想要的名字作为新函数名字
  3. 在新函数内做我们的变更(新增参数、删除参数、改变参数释义等)
  4. 改变函数调用的地方(如果是新增、修改、删除参数)
  5. 测试
  6. 对旧函数使用内联函数来调用或返回新函数
  7. 如果使用了临时名字,使用改变函数声明将其改回原来的名字(这时候就要删除旧函数了)
  8. 测试
  • 关键字:

内部重构、提炼新函数、好搜索的临时名字、变更、改变调用、旧函数使用新函数、改变调用名字

封装变量
  • 时机:
  1. 当我们在修改或者增加使用可变数据的时候
  2. 数据被大范围使用(设置值)
  3. 对象、数组无外部变动需要内部一起改变的需求时候,最好返回一份副本
  • 做法:
  1. 创建封装函数(包含访问和更新函数)
  2. 修改获取这个变量和更新这个变量的地方
  3. 测试
  4. 控制变量外部不可见(可以借助es6类中的get来实现不可变量以及限制可见)
  5. 测试
  • 关键字:

新函数、替换调用、不可见

变量改名
  • 时机:
  1. 变量/常量的名字不足以说明字段的意义
  2. 垃圾命名
  • 做法:
  1. 针对广泛使用的 1.1 先用封装变量手法封装 1.2 找到所有使用该变量的代码,修改测试(如果是对外已发布的变量,可以标记为不建议使用(作者没提到,但是个人感觉是可以这样的) 1.3 测试
  2. 只作用于某个函数的直接替换即可
  3. 替换过程中可以以新名字作为过渡。待全部替换完毕再删除旧的名字
  • 关键字:

封装变量手法、替换名字、中间过渡

引入参数对象
  • 时机:
  1. 一组参数总在一起出现
  2. 函数参数过多
  • 做法:
  1. 创建一个合适的数据结构(如果已经有了,可以略过) 数据结构选择:一种是以对象的形式,一种是以类的形式,作者推荐以类的形式,但是在我看来,要根据场景,如果这组数据以及其相关行为可以变为一组方法,如数组类里面的比较两个数组是否完全一致,这就可以以类来声明(js中也可以以export来导出而使用)
  2. 使用改变函数声明手法给原函数增加一个参数为我们新的结构
  3. 测试
  4. 旧数据中的参数传到新数据结构(变更调用方)
  5. 删除一项旧参数,并将之使用替换为新参数结构
  6. 测试
  7. 重复5、6
  • 关键字:

新结构、增加参数、入参新结构、删除旧参数、使用新结构

函数组合成类
  • 时机:
  1. 一组函数(行为)总是围绕一组数据做事情
  2. 客户端有许多基于基础数据计算派生数据的需求
  3. 一组函数可以自成一个派系,而放在其他地方总是显得不够完美
  • 做法:
  1. 如果这一组数据还未做封装,则使用引入参数对象手法对其封装
  2. 运用封装记录手法将数据记录封装成数据类
  3. 使用搬移函数手法将已有的函数加入类(如果遇到参数为新类的成员,则一并替换为使用新类的成员)
  4. 替换客户端的调用
  5. 将处理数据记录的逻辑运用提炼函数手法提炼出来,并转为不可变的计算数据
  • 关键字:

提炼变量、封装成类、移入已有函数、替换调用、移入计算数据

函数组合成变换
  • 时机:
  1. 函数组合成变换手法时机等同于组合成类的手法,区别在于其他地方是否需要对源数据做更新操作。 如果需要更新则使用类,不需要则使用变换,js中推荐类的方式
  • 做法:
  1. 声明一个变换函数(工厂函数)
  2. 参数为需要做变换的数据(需要deepclone)
  3. 计算逻辑移入变换函数内(比较复杂的可以使用提炼函数手法做个过渡)
  4. 测试
  5. 重复3、4
  • 关键字:

变换函数、变换入参、搬移计算逻辑

封装记录
  • 时机:
  1. 可变的记录型结构
  2. 一条记录上有多少字段不够直观
  3. 有需要对记录进行控制的需求(个人理解为需要控制权限、需要控制是否只读等情况)
  4. 需要对结构内字段进行隐藏
  • 做法:
  1. 首先用封装变量手法将记录转化为函数(旧的值的函数)
  2. 声明一个新的类以及获取他的函数
  3. 找到记录的使用点,在类内声明设置方法
  4. 替换设置值的方法(es6 set)
  5. 声明一个取值方法,并替换所有取值的地方
  6. 测试
  7. 删除旧的函数
  8. 当我们需要改名时,可以保留老的,标记为不建议使用,并声明新的名字进行返回
  • 关键字:

转化函数、取值函数、设值函数、替换调用者、替换设置者

以对象取代基本类型
  • 时机:
  1. 随着开发迭代,我们一个简单的值已经不仅仅只是简单的值那么简单了,他可能还要肩负一些其他的职责,如比较、值行为等
  2. 一些关键的、非仅仅只有打印的功能的值
  • 做法:
  1. 如果没被封装,先使用封装变量手法
  2. 为要修改的数据值创建一个对象,并为他提供取值、设值函数(看需求)
  3. 使用者(可能是另外一个大类)修改其取值设值函数
  4. 测试
  5. 修改大类中的取值设值函数的名称,使其更好的语义化
  6. 为这个新类增加其行为(可能是转换函数、比较函数、特殊处理函数、操作函数)等
  7. 根据实际需求对新类进行行为扩展(如果有必要的话)
  8. 修改外部客户端的使用
  • 关键字:

新类、取设值函数、行为入类、扩展类

以查询取代临时变量
  • 时机:
  1. 修改对象最好是一个类(这也是为什么提倡class,因为类可以开辟一个命名空间,不至于有太多全局变量)
  2. 有很多函数都在将同一个值作为参数传递
  3. 分解过长的冗余函数
  4. 多个函数中重复编写计算逻辑,比如讲一个值进行转换(好几个函数内都需要这个转换函数)
  5. 如果这个值被多次修改,应该将这些计算代码一并提炼到取值函数
  • 做法:
  1. 检查是否每次计算过程和结果都一致(不一致则放弃)
  2. 如果能改为只读,就改成只读
  3. 将变量赋值取值提炼成函数
  4. 测试
  5. 去掉临时变量
  • 关键字:

只读、提炼函数、删变量

提炼类
  • 时机:
  1. 一个大的类在处理多个不同的事情(这个类不纯洁了)
  • 做法:
  1. 确定分出去的部分要做什么事情
  2. 创建一个新的类,表示从旧地方分离出来的责任
  3. 旧类创建时,为新类初始化
  4. 使用搬移函数手法将需要的方法搬移到新的类(搬移函数时候就将调用地方改名)
  5. 删除多余的接口函数,并为新类的接口取一个适合自己的名字
  6. 考虑是否将新的类开放为公共类
  • 关键字:

职责边界确认、创建新域、新旧同步初始化、行为搬家、接口删除

内联类
  • 时机:
  1. 一个曾经有很多功能的类,在重构过程中,已经变成一个毫无单独职责的类
  2. 需要对两个类重新进行职责划分
  • 做法:
  1. 将需要内联的类中的所有对外可调用函数(也可能是字段)在目标类中新建一个对应的中间代理函数
  2. 修改调用者,调用代理方法并测试
  3. 将原函数中的相关方法(字段)搬移到新地方并测试
  4. 原类变为空壳后就可以删除了
  • 关键字:

代理、修改调用者、方法搬家、抛弃旧类

隐藏委托关系
  • 时机:
  1. 一个类需要隐藏其背后的类的方法或事件
  2. 一个客户端调用类的方法时候,必须知道隐藏在后面的委托关系才能调用
  • 做法:
  1. 在服务类(对外的类)中新建一个委托函数,让其调用受托类(背后的类)的相关方法
  2. 修改所有客户端调用为这个委托函数
  3. 重复12直到受托类全部被搬移完毕,移除服务类中返回受托类的函数
  • 关键字:

委托函数、替换调用者、删除委托整个类

移除中间人
  • 时机:
  1. 因为隐藏委托关系(当初可能是比较适合隐藏的)手法造成的现在转发函数越来越多
  2. 过度的迪米特法则造成的转发函数越来越多
  • 做法:
  1. 在服务类(对外)内为受托对象(背后的类)创建一个返回整个委托对象的函数
  2. 客户端的调用转为连续的访问函数进行调用
  3. 删除原本的中间代理函数
  • 关键字:

委托整个类、修改调用、删除代理

替换算法
  • 时机:
  1. 旧算法已经不满足当前功能
  2. 有更好的方式可以完成与旧算法相同的事情(通常是因为优化)
  • 做法:
  1. 保证待替换的算法为单独的封装,否则先将其封装
  2. 准备好更好的算法,
  3. 替换算法过去
  4. 运行并测试新算法与旧算法对比(一定要对比,也许你选的还不如以前呢)
  • 关键字:

算法封装、编写新算法、替换算法、比较算法

搬移函数
  • 时机:
  1. 随着对项目(模块)的认知过程中,也可能是改造过程中,一些函数已经脱离了当前模块的范围
  2. 一个模块内的一些函数频繁的与其他模块交互,却很少和自身内部进行交互(出现了叛变者)
  3. 一个函数在发展过程中,现在他已经有了更通用的场景
  • 做法:
  1. 查找要搬移的函数在当前上下文中引用的所有元素(先将依赖最少的元素进行搬离)
  2. 考虑待搬移函数是否具有多态性(复写了超类的函数或者被子类重写)
  3. 复制函数到目标上下文,调整函数,适应新的上下文
  4. 函数内使用的变量考虑是一起搬移还是以参数传递
  5. 改写原函数为代理函数(也可以内联)
  6. 检查新函数是否可以继续进行搬离
  • 关键字:

确定关系、确定继承、优先基础、函数搬家、相关部分位置确定、原址代理、优化新函数

搬移字段
  • 时机:
  1. 随着业务推进过程中,原有的数据结构已经不能很好的表示程序的逻辑
  2. 每当调用一个函数时,需要传入的记录参数,总是需要传入另一条记录或者他的某些字段一起
  3. 修改(行为)一条记录时,总是需要同时改动其他记录
  4. 更新(数据)一条字段时,总是需要同时在多个结构中作出修改
  • 做法:
  1. 源字段已经被封装(如果未封装,则应该先使用封装变量手法对其封装)
  2. 目标对象上创建一个字段,及其访问函数
  3. 源对象对目标对象的字段做对应的代理
  4. 调整源对象的访问函数,令其使用目标对象的字段
  5. 测试
  6. 移除源对象的字段
  7. 视情况而定决定是否需要内联变量访问函数
  • 关键字: 封装、新字段、源址代理、代理新址、旧字段移除、确定是否内联
搬移语句到函数
  • 时机:
  1. 重复代码
  2. 每次调用a方法时,b操作也总是每次都执行
  3. 某些语句放在特定函数内更像一个整体
  • 做法:
  1. 将重复代码使用搬移函数手法到紧邻目标函数的位置
  2. 如果目标函数紧被唯一一个原函数调用,则只需要将原函数的重复片段粘贴到目标函数即可
  3. 选择一个调用点进行提炼函数,将目标语句函数与语句提炼成一个新的函数
  4. 修改函数其他调用点,令他们调用新提炼的函数
  5. 调整函数的引用点
  6. 内联函数手法将目标函数内联到新函数里
  7. 移除原目标函数
  8. 对新函数应用函数改名手法(改变函数声明的简单做法)
  • 关键字:

代码靠近、单点提炼、中间函数、修改引用、函数内联、原函数删除、函数改名

函数搬移到调用者
  • 时机:
  1. 随着系统前进过程中,函数某一块的作用发生改变,不再适合原函数位置
  2. 之前在多个地方表现一致的行为,如今在不同调用点面前表现了不同的行为

tips: 本手法只适合边界有些许偏移的场景,不适合相差较大的场景

  • 做法:
  1. 简单情况下,直接剪切
  2. 将不想搬移的部分提炼成与当前函数同级函数(如果是超类方法,子类也要一起提炼)
  3. 原函数调用新的同级函数
  4. 替换调用点为新的同级函数和要内联的语句
  5. 删除原函数
  6. 使用函数改名手法(改变函数声明的简单做法)改回名字
  • 关键字:

提炼不变的为临时方法、搬移语句、删除原,改名字

以函数调用替换内联代码
  • 时机:
  1. 函数内做的某些事情与已有函数重复
  2. 已有函数与函数之间希望同步变更
  • 做法:
  1. 内联代码替换为函数(可能有参数 就要对应传递)
  • 关键字:

内联替换

移动语句
  • 时机:
  1. 移动语句一般用于整合相关逻辑代码到一处,这是其他部分手法的基础
  2. 代码相关逻辑整合一处方便我们对这部分代码优化和重构
  • 做法:
  1. 确定要移动的语句要移动到哪(调整的目标是什么、该目标能否达到)
  2. 确定要移动的语句是否搬移后会使得代码不能正常工作,如果是,则放弃
  • 关键字:

确定副作用、确定目标

拆分循环
  • 时机:
  1. 一个循环做了多件不相干事
  • 做法:
  1. 复制循环
  2. 如果有副作用则删除单个循环内的重复片段
  3. 提炼函数
  4. 优化内部
  • 关键字:

复制循环、行为拆分、函数提炼

以管道替代循环
  • 时机:
  1. 一组虽然在做相同事情的循环,但是内部过多的处理逻辑,使其晦涩难懂
  2. 不合适的管道(如过滤使用some)
  • 做法:
  1. 创建一个新变量,用来存放每次行为处理后,参与循环的剩余集合
  2. 选用合适的管道,将每一次循环的行为进行搬移
  3. 搬移完所有的循环行为,删除整个循环
  • 关键字:

新变量、合适的管道、删除整个循环

移除死代码
  • 时机:
  1. 代码随着迭代已经变得没用了。
  2. 即使这段代码将来很有可能还会使用,那也应该移除,毕竟现在版本控制很实用。
  • 做法:
  1. 如果不可以外部引用,则放心删除(如果可能将来极有可能会启用,在这里留下一行注释,标示曾经有过这段代码,以及它被删除的那个提交的版本号) 2、如果外部引用了,则需要仔细确认还有没有其他调用点(有eslint规则限制的话。其实可以先删了,看有没有报错)
  • 关键字:

检查引用

拆分变量
  • 时机:
  1. 一个变量被应用到两种/多种的作用下
  2. 修改输入参数的值
  • 做法:
  1. 在变量第一次赋值的地方,为函数取一个更加有意义的变量名(尽量声明为const)
  2. 在第二次赋值地方声明该变量
  3. 以该变量第二次赋值动作为界,修改此前对该变量的所有引用。让他们引用新的变量
  4. 测试
  5. 重复上述,直到变量拆分完毕
  • 关键字:

新变量、赋值时声明、替换调用

字段改名
  • 时机:
  1. 记录结构中的字段需要改个名字
  • 做法:
  1. 如果结构简单,可以一次性替换
  2. 如果记录没有封装,最好是先封装记录
  3. 修改构造时候做兼容判断(老的值与新的值兼容判断:this.a = data.a || data.b)
  4. 修改内部设取值函数
  5. 修改记录数据类中的内部调用
  6. 测试
  7. 修改外部调用初始化时候的数据
  8. 删除初始化兼容判断
  9. 使用函数改名手法(改变函数声明的简单做法),修改调用处的调用方式及内部取设值函数为新字段名
  • 关键字:

封装、兼容初始化、内部取设只返回新字段,修改内部调用,测试、删除兼容、内部取设改名、替换外部调用

以查询取代派生变量
  • 时机:
  1. 两个变量相互耦合
  2. 设置一个变量的同时,将另一个变量与该变量结合 通过计算后给另一个变量设置值

tips:计算的参考变量,是不可变的,计算结果也是不可变的。可以不重构(还是那句话,不可变的数据,我们就没必要理他)

  • 做法:
  1. 确定可以引起变量发生变化的所有点(如果有来自其他模块变量,需要先用拆分变量手法)
  2. 新建一个计算函数,计算变量值
  3. 引入断言(assert),确保计算函数的值与该变量结果相同
  4. 测试
  5. 修改读取变量的代码,用内联函数手法将计算函数内联进来)
  6. 用移除死代码手法将旧的更新点的地方清理掉
  • 关键字:

来源确定、结果相同、计算函数、清理更新点

将引用对象改为值对象
  • 时机:
  1. 几个对象中共享了一个对象,并且要联动变更的情况下
  2. 值对象就是每次设置都直接设置这个值,比如:

值对象:a.b=new b(1) 引用对象:a.b.c=1

  • 做法:
  1. 检查重构的目标是否为不可变对象,如果不是的话,则看看是否可以将其改为不可变对象
  2. 用移除设值函数手法去掉第一个设引用值函数(每次都用设置值的方式复写整个对象)
  3. 测试
  4. 重复2、3
  5. 判断两次相同输入时候,值是否相等
  • 关键字:

不可变、替换设置引用值为设置值

将值对象改为引用对象
  • 时机:
  1. 数据副本在多处使用,并且需要一处变化其他地方同步更新
  • 做法:
  1. 创建一个仓库(如果没有的话),仓库要支持:每次访问相同数据都是一个相同的引用对象、支持注册新数据和获取同一个引用数据(js可以在简单场景下简单的使用{})
  2. 确保仓库的构造函数有办法找到关联对象的正确实例
  3. 修改调用点,令其从仓库获取关联对象。
  4. 测试
  • 关键字:

共享仓库、单例的引用对象、替换调用点

分解条件表达式
  • 时机:
  1. 条件逻辑内,过长的函数,导致反而难以理解条件逻辑的场景
  2. 单个条件逻辑处理的函数过大
  • 做法:
  1. 对条件判断的每个分支分别运用提炼函数手法
  2. 如果条件表达式过长,对条件表达式运用提炼函数手法
  3. 优化当前条件逻辑(如使用三元表达式)
  • 关键字:

提炼分支、提炼条件、优化判断

合并条件表达式
  • 时机:
  1. 无其他副作用的嵌套if
  2. 无其他副作用的,且返回一致的并列if
  3. 这些if都是关联的(可以用是否能提炼出一个合适的函数名来作为依据,但也不是绝对,我们可以选择不提炼函数,但是还是建议是相关的if作为一组)
  • 做法:
  1. 确定条件表达式有副作用,先用将查询函数和修改函数分离的手法对其处理 2、如果是嵌套函数一般是用逻辑与合并,如果是并列的if一般是用逻辑或合并,如果两种均有,就要组合使用了(但是我更建议他们应该分离成多个判断) 3、测试
  2. 重复2、3
  3. 对合并后的条件表达式进行提炼函数手法(有必要的话)
  • 关键字:

分离副作用、合适的逻辑符、提炼条件函数

以卫语句取代嵌套表达式
  • 时机:
  1. 无其他副作用的嵌套if
  2. 无其他副作用的,且返回一致的并列if
  3. 这些if都是关联的(可以用是否能提炼出一个合适的函数名来作为依据,但也不是绝对,我们可以选择不提炼函数,但是还是建议是相关的if作为一组)
  • 做法:
  1. 选取最外层需要被替换的条件逻辑,将其替换为卫语句(单独检查条件、并在条件为真时立刻返回的语句,叫做卫语句)
  2. 测试
  3. 重复1、2
  • 关键字:

从外而内

以多态取代条件表达式
  • 时机:
  1. 多种并列或者嵌套的条件逻辑,让人难以理解
  2. switch
  3. 同行为不同类型的判断
  • 做法:
  1. 确定现有的条件类是否具有多态性,如果没有,可以通过将行为封装成类(借助其他手法如函数组合成类等)
  2. 在调用方使用工厂函数获得行为对象的实例
  3. 针对不同类型创建子类(相当于在超类在分化)
  4. 调用方此时应当通过一个工厂返回合适的子类
  5. 将超类中针对子类类型所做的判断,逐一移入对应子类进行复写(相关子类复写超类的分支函数),超类只留下默认值

注意:这种手法其实是在面向对象开发中很常用的一种方式,但是如果不是

  1. 在写一个面向对象很明确的项目
  2. 这个判断过于大
  3. 可以明确这些子类抽取出来是有意义的(从后期维护角度来说,需要对其增加一些行为)
  4. 这个子类可以自成体系

不如将其通过一个json或者map来进行指责划分。在js中我觉得更常用的是以策略来代替if

  • 关键字:

多态、继承、封装、行为拆分

引入特例
  • 时机:
  1. 数据结构的调用者都在检查某个特殊值,并且这个值每次所做的处理也都相同
  2. 多处以同样方式应对同一个特殊值

三种情况 第一种原始为类,特例元素没有设置值的操作 第二种原始为类,特例元素有设置值的操作 第三种 原始就是普通的json

  • 做法:
  • 针对于有自己对应行为的类
  1. 在原类中为特例元素增加一个函数,用以标记这个特例的情况,默认返回一个写死的就行)
  2. 为特例创建一个class,用以处理特例的正常逻辑和行为,需要把特例对象及其所有行为放到这个类
  3. 将本次特例的条件使用提炼函数手法抽成一个在类中的字段函数返回true
  4. 修改所有调用者为第3步的函数
  5. 修改第一步创建的类。让它返回我们的特例对象
  6. 特例中的其他字段
  • 针对于只读的类
  1. 将上面做法的创建一个b类改为在类内创建一个函数,返回对象即可。把特例所需信息全部返回在js
  • 针对于原始不是类的
  1. 为特例对象创建一个函数,返回特例对象的深拷贝状态
  2. 将本次特例的条件使用提炼函数手法抽成一个统一的函数
  3. 对第一步创建的函数返回值做特殊增强。 将需要的特例的值,逐一放进来。
  4. 替换调用者使用函数的返回值
  • 关键字:

特例逻辑搬到class、过渡函数、替换调用者、修改新class

将查询函数和修改函数分离
  • 时机:
  1. 一个函数既有返回值又有设置值
  • 做法:
  1. 复制一份目标函数并改名为查找函数的名字
  2. 将被复制的函数删除设置值的代码
  3. 将调用者替换为新函数,并在下面调用原函数
  4. 删除原函数返回值
  5. 将原函数和新函数中的相同代码进行优化
  • 关键字:

新函数为查找、删除设置值、替换调用者、删除返回值、优化

函数参数化
  • 时机:
  1. 有多余一个函数的逻辑非常相似,只是有一些字面量不同(有时候可能会碰到a、b很相似,a、c也很相似,但是b、c差距比较大时候,这种情况个人观点为:将ab、ac中逻辑紧密的抽成一个,不要形式化的就要吧abc抽到一起。反而适得其反)
  • 做法:
  1. 从这一组相似函数中,找到一组,通常来说尽可能选择调用比较少的地方
  2. 运用改变函数声明手法(改变参数)使其在调用时候,将变化的部分以参数形式传入)
  3. 修改当前这个函数的所有调用点,为调用新函数,并传递参数
  4. 修改新函数,让它使用新传进来的参数
  5. 将其他相似的函数,逐一替换为这个新函数,每次替换都要测试一下
  • 关键字:

调用较少、变化点入参、修改调用、替换使用

移除标记参数
  • 时机:
  1. 一个用来控制函数流程的参数
  • 做法:
  1. 针对参数的每一种可能值,新建一个明确函数(如果参数控制整个流程,则可以用分解条件表达式手法创建明确函数,如果只控制一部分函数则创建转发函数,将这些函数,统一通过这些明确函数进行转发)
  2. 替换调用者

tips:如果是这个标记即作为标记,又作为参数值。则对其进行拆分。

  • 关键字:

流程、行为拆分

保证对象完整的手法
  • 时机:
  1. 从一个代码中导出几个值
  2. 调用者将自身的部分参数传递
  3. 一般发生在引入参数对象手法之后
  • 做法:
  1. 新建一个空函数(可能是新建,也可能是用提炼函数),接受完整对象
  2. 新函数体内调用旧函数,并且使用合适的参数列表
  3. 修改旧函数的调用者,令他使用新函数,修改旧函数内部
  4. 使用内联函数手法将旧函数内代码搬移到新建的函数 5、修改新函数的名字为旧函数
  • 关键字:

接受完整对象、新调用老、修改调用、内联、改名

以查询取代参数
  • 时机:
  1. 一个函数传入了多个相同的值(如总是能根据b参数不需要很复杂就可以查到a参数)
  2. 调用函数传入了一个函数本身就可以很容易获得的参数(指的是内部或者计算获得,而非从其他模块拿)
  3. 如果目标函数本身就具有引用透明性(函数的返回值只依赖于其输入值),用查询后,他去访问了一个全局变量,则不适合用本重构

一言以概之:这个函数自身或者通过参数都能得到另一个值就可以使用这个手法

  • 做法:
  1. 如果有必要,可以将参数计算的过程提炼为一个只读变量或者一个函数
  2. 将函数体内引用该参数的地方,都改为运用计算函数
  3. 去掉该参数(调用者也要去掉)
  • 关键字:

提炼变量、参数消除

以参数取代查询
  • 时机:
  1. 一个函数内部因为引用了全局变量而导致了不透明
  2. 一个函数内部引用了一个即将被删除的元素
  3. 一个函数内部,过多的依赖了另一个模块(这种有两种做法:一种是本手法,另一种是搬移函数手法,要根据函数实际作用操作
  • 做法:
  1. 使用提炼变量手法将目标(希望作为参数传入的查询)提炼出来
  2. 把整个函数体提炼,并且单独放到一个函数内(需要保留计算逻辑,计算逻辑作为代理函数每次的值以参数传入函数)
  3. 消除刚才提炼出来的变量(旧函数应该只剩下一个简单的调用)
  4. 修改调用方,改为调用新函数,并传入调用时候计算的计算值
  5. 删除原函数内的计算代理
  6. 新函数改回旧函数的名字(如果意义发生变化,需要重新起名字)
  • 关键字:

变量提炼、函数体换新、旧函数传参、旧函数调新函数,删除代理函数、函数改名

移除设值函数
  • 时机:
  1. 类内某个字段有一些设值函数
  2. 类无任何副作用(如:操作渲染html的append、往localstorage写东西、init调用接口、多处共享引用等)
  3. 很庞大的类(需要先作拆分优化)
  • 做法:
  1. 如果无法拿到设置变化的值,就通过构造函数的参数传入
  2. 在构造函数内部调用设值函数进行更新
  3. 移除所有的设置值的函数调用,改为new一个类
  4. 使用内联函数手法消除设值函数。

tips:可以批量操作多个设值函数。

  • 关键字:

设值替换为new

以工厂函数取代构造函数
  • 时机:
  1. 构造函数每次都需要new关键字,又臭又长(个人观点是这条没必要,除非完全忍受不了)
  2. 构造函数如果不是default导出的话,这个名字那就是固定的。有时候语义化不明显
  3. 有时虽然都是调用同一个类。但所处环境不同,我调用意义就不同
  • 做法:
  1. 新建一个工厂
  2. 工厂调用并返回现有的构造函数
  3. 替换调用者
  4. 尽可能缩小构造函数可见范围(js中很难实现,可能只能藏的深一些)
  • 关键字:

工厂函数、调用类、替换调用

以命令取代函数手法
  • 时机:
  1. 在js中,体现为又臭又长的还没法进行指责划分的函数(可能是它们都属于同一部分逻辑,也可能是因为内部写法导致不好划分)
  • 做法:
  1. 新建一个空的类
  2. 用搬移函数手法将函数搬移到这个新的类
  3. 给类改个有意义的名字,如果没什么好名字就给命令对象的实际具体执行的函数起一个通用的名字,如:execute或者call
  4. 将原函数作为转发函数,去构造类
  5. 将函数内的参数,改为构造时候传入
  6. 如果可以将其他字段修改为只读
  • 关键字:

新的类、函数搬家、原类转发函数、构造入参、只读

函数上移手法
  • 时机:
  1. 子类中有绝大部分都在复制某个函数
  2. 这些函数函数体都相同或者近似
  • 做法:
  1. 确保待提升函数的行为完全一致,否则需要先将他们一致化
  2. 检查函数体内的所有调用和字段都能从超类中调用(如果有不一致则考虑先把它们提升)
  3. 检查函数名字全部一致,不一致的话先将他们名字统一
  4. 将函数复制到超类中
  5. 逐一移除子类中的函数。每一次都要测试
  • 关键字:

函数体一致化、名字一致化、引用调用先行、提升函数、删除重写

字段上移手法
  • 时机:
  1. 子类中有绝大部分都在复制某个字段
  • 做法:
  1. 检查该字段的所有使用点,确保是在同样的方式被使用
  2. 如果名字不同,先把名字统一化
  3. 移动到父类,并确保子类都能访问父类的这个字段
  4. 逐一移除子类的该字段
  • 关键字:

同样方式使用、统一名字、字段上移、删除子类字段

构造函数本体上移
  • 时机:
  1. 子类中有绝大部分都在复制某个构造函数函数
  2. 这些构造函数函数体都相同或者近似
  • 做法:
  1. 如果超类没有构造函数,就先定义一个,所有子类增加super关键字
  2. 使用移动语句将子类的公共语句移动到super紧挨着之后
  3. 提升到超类构造函数中
  4. 逐一移除子类的公共代码,如果这个值来自于调用者,则从super上传给父类
  5. 如果要上移的语句有基于子类的字段而设置初始化的值的,查看是否可以将这个字段上移,如果不能,则使用提炼函数语句,将这句提炼为一个函数,在构造函数内调用他
  6. 函数上移
  • 关键字:

构造函数内的语句上移

函数下移、字段下移
  • 时机:
  1. 超类中的函数(字段)只与一部分子类有关(这个范围需要掌控好,我通常选择如果使用超过三分之二的,并且在剩余的三分之一里面,这个函数/字段没有副作用,就选择上移,否则下移)
  • 做法:
  1. 将超类中的函数(字段)本体逐一复制到每一个需要此函数(字段)的子类中
  2. 删除超类中的函数(字段)
  • 关键字:

按需放置

以子类取代状态码
  • 时机:
  1. 一个类中有一些有必要的多态性被隐藏
  2. 根据某个状态码来返回不同的行为
  • 做法:
  • 直接继承超类的
  1. 将类型码字段进行封装,改为一个get type()的形式
  2. 选择其中一个类型码,为其创建一个自己类型的子类
  3. 创建一个选择器逻辑(根据类型,选择正确的子类)把类型码复制到新的子类
  4. 测试
  5. 逐一创建、添加选择逻辑的代码
  6. 移除构造函数的这个参数
  7. 将与类型相关的代码重构优化
  • 间接继承(通过类型的超类而非现有超类进行继承)
  1. 用类型类包装类型码(以对象取代基本类型手法)
  2. 走直接继承超类的逻辑,唯一不同的是,这次要继承类型超类,而非当前超类
  1. 关键字:

封装类型码、多态化、选择子类的函数、移除类型参数

移除子类
  • 时机:
  1. 随着程序发展子类原有行为被搬离殆尽
  2. 原本是为了适应未来,而增加子类,但是现在放弃了这部分代码。
  3. 子类的用处太少,不值得保留
  • 做法:
  1. 检查子类的使用者,是否根据不同子类进行处理
  2. 如果处理了则将处理函数封装为一个函数,并将他们搬移到父级
  3. 新建一个字段在超类,用以代表子类的类型
  4. 将选择哪个类来实例化的构造函数搬移到超类
  5. 逐步搬移所有的类型
  6. 将原本的类型处理改为使用新建的字段进行判断处理
  7. 删除子类
  • 关键字:

工厂函数取代子、类型提炼、检查类型判断

提炼超类
  • 时机:
  1. 两个类在做类似的事情
  2. 两个类随着程序发展,有一些共同部分需要合并到一起
  • 做法:
  1. 新建超类(可能已经存在)
  2. 调整构造函数(从数据开始)
  3. 调整子类需要的字段
  4. 将多个子类内共同的行为复制到超类
  5. 检查客户端代码。考虑是否调整为超类
  • 关键字:

相同事情搬移到超类

以委托取代子类
  • 时机:
  1. 类只能继承一个,无法多继承
  2. 继承给类引入了紧密的关系(超类、子类耦合严重)
  • 做法:
  1. 使用以工厂函数取代构造函数将子类封装
  2. 创建一个委托类、接受所有子类的数据,如果用到了超类,则以一个参数指代超类
  3. 超类中增加一个安放委托类的字段
  4. 增加一个创建子类的工厂,让他初始化超类中的委托字段
  5. 将子类中的函数搬移至委托类,不要删除委托代码(如果用到了其他元素也要一并搬离)
  6. 如果这个函数被子类之外使用了,把留在子类的委托移动到超类中,并加上卫语句,检查委托对象初始化
  7. 如果没有其他调用者,使用移除死代码手法去掉没人使用的委托代码
  8. 测试
  9. 重复567。直到所有函数都搬到了委托类
  10. 找到调用子类的地方,将其改为使用超类的构造函数
  11. 去掉子类
  • 关键字:

工厂函数初始化类、委托类、所有子类数据搬移至委托类、超类增加委托类的字段、子类函数搬移到委托类、删除子类

以委托取代超类手法
  • 时机:
  1. 错误的继承(如父子不是同一个意义的东西,但是子还想要用超类的一些字段)
  2. 超类不是所有方法都适用于子类
  • 做法:
  1. 在子类中创建一个属性,指向新建的超类实例
  2. 子类中用到超类的函数,为他们创建转发函数(用上面的属性)
  3. 去除子类与超类的继承关系
  • 关键字:

子类属性指向超类、转发函数、去除继承

读后感:

收获最大的莫过于感叹作者过于谨慎,震惊于作者重构能力之强,对代码重构理解程度之深,虽然作者有一些“墨迹”,但不可否认,这是极佳的一种方式,虽然工作中我们不得已没有那么多时间去这么小的步子,我们可以步子稍微大一些,当遇到问题时,在回滚并放慢步子。

第二点收获就为作者对于代码好坏的定义,好的代码就是让人能够理解,能够让人很快的找到自己要修改的地方,并可以高效的规避报错风险。虽然前期投入时间可能会多一些,但后期的效果却是让人能够惊讶的,正如作者所说的:清晰的代码更容易理解,使你能够发现更深层次的设计问题,从而形成积极正向的循环。

作者一直在强调,重构是一步一步改进的,不是说一下子就要如何如何,不只是说单次改进过程要小幅度多测试,也是在说我们不一定要将代码中所有都实现到近乎完美的地步,而是应该抉择一个代码重构与真实情况的平衡点,这和大刀阔斧直谈架构重构的也不一样,代码是在不断构筑-设计中保持自己的新鲜性,直谈大型架构重构,只能是笑谈,毕竟架构为设计、编写。而直接重构整个架构,除非你想被老板炒鱿鱼了。

最后本章作者使用了很多的手法,虽然都只是一些常用的什么提炼函数啊、内联变量啊之类的,但处处又透露着我们需要学习的!

重构是为了代码能被人读懂(所谓什么更好扩展啊、更好的设计模式啊、结构化啊等等等等都是为了这点。所以我统归为为了能读懂)可以选择牺牲一些(不是说可以完全忽略了性能),毕竟在现代浏览器、打包工具的加持、缓存的加持下,肉眼看到的问题以及我们思考的问题也许已经被各种加持下悄悄消失了

从长久来看,重构对于日后的维护日后的开发,随着时间的流逝肯定是一个正收益,但是短期来说可能要影响我们一些,我们要权衡好这些点之间的平衡,毕竟工作是为了赚钱,公司也是为了盈利,不可能给我们无限时间去搞这些,作者同时也提出了重构并不是工作日志中的某一个任务的时机,主要体现在:新功能开发时、为了代码可读性、代码整合、有计划的重构代码以及坚持长期的重构以及review的重构。可以说是随时随地都可以重构,但也不是任何地方任何时机都可以重构,我们要利用好测试的套件,保证原效果的前提下,结合实际情况,多维度思考,即使阅读过后,也应该时常翻开这本书,进行反复阅读,以提醒自己。

在本书中我学到了如何甄别坏的代码,以及怎么处理他们,学到了开发中应该测试先行以及一些重构的基本知识。

不过作为jser,我个人觉得虽然作为一本通用型书籍,确实应该不掺杂很多的语法,不过既然选定了js,自动化重构这块其实怎么说呢,写的都是IDE。但是js中并没有这类工具。然后作者也没有说在js下应该可以借助某些功能来帮助重构。所以这块还是一片空白,虽然这种事理应自己去研究。但是还是想免费更好嘛。

原文出处

https://zhuanlan.zhihu.com/p/338146387

猜您喜欢: