• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

《重构改善既有代码的设计》读书笔记(上)

武飞扬头像
聂炳玉
帮助1

第一章:重构第一个示例

  第一章作者通过一个示例讲解重构的过程。由于需求变化或代码不易于理解需要进行重构,重构前需要有一个可靠的测试,重构的过程应该是小步修改,每次修改后就运行测试,测试过程中可以先忽略性能问题。

1. 重构过程

1.1 需求变化

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

1.2 重构的第一步——可靠的测试

  进行重构的时候,得确保即将修改的代码拥有一组可靠的测试。这些测试必须有自我检验能力。

1.3 小步修改,每次修改后就运行测试

  无论每次重构多么简单,养成重构后即运行测试的习惯非常重要。这就是重构过程的精髓所在:小步修改,每次修改后就运行测试。如果我改动了太多东西,犯错时就可能陷入麻烦的调试,并为此耗费大把时间。小步修改,以及它带来的频繁反馈,正是防止混乱的关键。

1.4 重构过程的性能问题

  大多数情况下可以忽略它。如果重构引入了性能损耗,先完成重构,再做性能优化。

1.5 好的命名

  好的命名十分重要,但往往并非唾手可得。但要一次把名取好并不容易,因此我会使用当下能想到最好的那个。如果稍后想到更好的,再将其换掉。

2. 结语

2.1 重构早期

  重构早期的主要动力是尝试理解代码如何工作。通常你需要先通读代码,找到一些感觉,然后再通过重构将这些感觉从脑海里搬回到代码中。清晰的代码更容易理解,使你能够发现更深层次的设计问题,从而形成积极正向的反馈环。

2.2 好代码的检验标准

  好代码的检验标准就是人们是否能轻而易举地修改它。好代码应该直截了当:有人需要修改代码时,他们应能轻易找到修改点,应该能快速做出更改,而不易引入其他错误。

2.3 好的代码库能提升生产力

  一个健康的代码库能够最大限度地提升我们的生产力,支持我们更快、更低成本地为用户添加新特性。为了保持代码库的健康,就需要时刻留意现状与理想之间的差距,然后通过重构不断接近这个理想。

2.4 高效有序的重构的关键

  小的步子可以更快前进,请保持代码永远处于可工作状态,小步修改累积起来也能大大改善系统的设计。

第二章:重构的原则

1. 何谓重构

1.1 何谓重构

  • 重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
  • 重构(动词):使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。
    名词解析——结构调整:
  • “结构调整”(restructuring)来泛指对代码库进行的各种形式的重新组织或清理,重构则是特定的一类结构调整。
  • 名词解析——可观察行为:
    可观察行为”的意思是,整体而言,经过重构之后的代码所做的事应该与重构之前大致一样。用户应该关心的行为而言,不应该有任何改变。

1.2 重构的关键

  重构的关键在于运用大量微小且保持软件行为的步骤,一步步达成大规模的修改。每个单独的重构要么很小,要么由若干小步骤组合而成。

2. 两顶帽子的概念

  Kent Beck提出了“两顶帽子”的比喻。使用重构技术开发软件时,我把自己的时间分配给两种截然不同的行为:添加新功能和重构。

2.1 添加新功能

  添加新功能时,我不应该修改既有代码,只管添加新功能。通过添加测试并让测试正常运行,我可以衡量自己的工作进度。

2.2 重构

  重构时我就不能再添加功能,只管调整代码的结构。此时我不应该添加任何测试(除非发现有先前遗漏的东西),只在绝对必要(用以处理接口变化)时才修改测试。

2.3 变换帽子

  软件开发过程中,可能会发现自己经常变换帽子。添加新功能和重构交替进行,无论何时都要清楚自己戴的是哪一顶帽子,并且明白不同的帽子对编程状态提出的不同要求。

3. 为何重构

3.1 重构改进软件的设计

3.1.1 程序在迭代中存在的问题

  如果没有重构,程序的内部设计(或者叫架构)会逐渐腐败变质。
  当人们只为短期目的而修改代码时,他们经常没有完全理解架构的整体设计,于是代码逐渐失去了自己的结构。程序员越来越难通过阅读源码来理解原来的设计。
  代码结构的流失有累积效应。越难看出代码所代表的设计意图,就越难保护其设计,于是设计就腐败得越快。

3.1.2 通过消除重复代码改善现有设计欠佳的程序

  设计欠佳的程序往往需要更多代码,这常常是因为代码在不同的地方使用完全相同的语句做同样的事,因此改进设计的一个重要方向就是消除重复代码。
  代码量减少将使未来可能的程序修改动作容易得多。代码越多,做正确的修改就越困难,因为有更多代码需要理解。

3.1.2 重构的好处

  经常性的重构有助于代码维持自己该有的形态。和优化程序设计。

####3.2 重构使软件更容易理解
  重构可以帮我让代码更易读。开始进行重构前,代码可以正常运行,但结构不够理想。在重构上花一点点时间,就可以让代码更好地表达自己的意图——更清晰地说出我想要做的。

3.3 重构帮助找到bug

  对代码进行重构,我们可以深入理解代码的所作所为,并立即把新的理解反映在代码当中。搞清楚程序结构的同时,也验证了自己所做的一些假设,容易将bug揪出来。
  Kent Beck经常形容自己的一句话:“我不是一个特别好的程序员,我只是一个有着一些特别好的习惯的还不错的程序员。”重构能够帮助我更有效地写出健壮的代码。

3.4 重构提高编程速度

3.4.1 不注重重构的系统

  不注重重构的系统,一开始开发进展很快,随着版本的不断迭代,添加新功能需要花越来越多的时间去考虑如何把新功能塞进现有的代码库,不断蹦出来的bug修复起来也越来越慢。

  代码库看起来就像补丁摞补丁,需要细致的考古工作才能弄明白整个系统是如何工作的。这份负担不断拖慢新增功能的速度,到最后程序员恨不得从头开始重写整个系统。

下面这幅图可以描绘他们经历的困境。
学新通

3.4.2 经常重构的系统

  经常重构的系统他们添加新功能的速度越来越快,因为他们能利用已有的功能,基于已有的功能快速构建新功能。
学新通

3.4.3 两者的区别

  两种团队的区别就在于软件的内部质量。需要添加新功能时,内部质量良好的软件让我可以很容易找到在哪里修改、如何修改。良好的模块划分使我只需要理解代码库的一小部分,就可以做出修改。如果代码很清晰,我引入bug的可能性就会变小,即使引入了bug,调试也会容易得多。理想情况下,我的代码库会逐步演化成一个平台,在其上可以很容易地构造与其领域相关的新功能。

3.4.4 设计耐久性假说

通过投入精力改善内部设计,我们增加了软件的耐久性,从而可以更长时间地保持开发的快速。

3.4.5 重构改善已有程序设计

  20年前,行业的陈规认为:良好的设计必须在开始编程之前完成,因为一旦开始编写代码,设计就只会逐渐腐败。
  重构改变了这个图景。现在我们可以改善已有代码的设计,因此我们可以先做一个设计,然后不断改善它,哪怕程序本身的功能也在不断发生着变化。由于预先做出良好的设计非常困难,想要既体面又快速地开发功能,重构必不可少。

4. 何时重构

4.1 三法则

  • 第一次做某件事时只管去做;
  • 第二次做类似的事会产生反感,但无论如何还是可以去做;
  • 第三次再做类似的事,你就应该重构。
  •  

正如老话说的:事不过三,三则重构。

4.2 预备性重构:让添加新功能更容易

  重构的最佳时机就在添加新功能之前。在动手添加新功能之前,我会看看现有的代码库,此时经常会发现:如果对代码结构做一点微调,我的工作会容易得多。
  修复bug时的情况也是一样。

4.3 帮助理解的重构:使代码更易懂

  重构的时机:当我们理解代码的时候需要思考“这段代码到底在做什么”。
  通过重构达到的目的:重构这段代码,令其一目了然。

4.4 捡垃圾式重构

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

4.5 有计划的重构

  如果团队过去忽视了重构,那么常常会需要专门花一些时间来优化代码库,以便更容易添加新功能。在重构上花一个星期的时间,会在未来几个月里发挥价值。有时,即便团队做了日常的重构,还是会有问题在某个区域逐渐累积长大,最终需要专门花些时间来解决。但这种有计划的重构应该很少,大部分重构应该是不起眼的、见机行事的。

4.6 见机行事的重构

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

4.7 长期重构

  可以在一个团队内,达成共识。每当有人靠近“重构区”的代码,就把它朝想要改进的方向推动一点。例如,如果想替换一个正在使用的库,可以先引入一层新的抽象,使其兼容新旧两个库的接口,然后一旦调用方完全改为了使用这层抽象,替换下面的库就会如容易的多。

4.8 复审代码时重构

  代码复审也让更多人有机会提出有用的建议,我们可以按照这些有用的建议对代码进行重构。
重构还可以帮助代码复审工作得到更具体的结果。不仅获得建议,而且其中许多建议能够立刻实现。最终你将从实践中得到比以往多得多的成就感。

4.9 何时不应该重构

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

5. 重构的挑战

5.1 延缓新功能开发

  实际上,这只是一部分不理解重构真正原因的人的想法,重构是为了从长效上见到收益,一段优秀的代码能让我们开发起来更顺手,要权衡好重构与新功能的时机,比如一段很少使用的代码。就没必要对他重构

5.2 代码所有权

  有时候我们经常会遇到,接口发布者与调用者不是同一个人,并且甚至可能是用户与我们团队的区别,在这种情况下,需要使用函数改名手法,重构新函数,并且保留旧的对外接口来调用新函数,并且标记为不推荐使用。

5.3 分支的差异

  经常会有长期不合并的分支,一旦存在时间过长,合并的可能性就越低,尤其是在重构时候,我们经常要对一些东西进行改名和变化,所以最好还是尽可能短的进行合并,这就要求我们尽可能的将功能颗粒化,如果遇到还没开发完成且又无法细化的功能,我们可以使用特性开关对其隐藏。

5.4 缺乏一组自测试的代码

  一组好的测试代码对重构很有意义,它能让我们快速发现错误,虽然实现比较复杂,但他很有意义。

5.5 遗留代码

  不可避免,一组别人的代码使得我们很烦恼,如果是一套没有合理测试的代码则使得我们更加苦恼。这种情况下,我们需要增加测试,可以运用重构手法找到程序的接缝,再接缝处增加测试,虽然这可能有风险,但这是前进所必须要冒的风险,同时不建议一鼓作气的把整个都改完,更倾向于能够逐步地推进。

5.6 数据库

  数据库重构最好是分散到多次生产发布来完成,这样即便某次修改在生产数据库上造成了问题,也比较容易回滚。

6. 重构、架构和YAGNI

6.1 早期的经验

在任何人开始写代码之前,必须先完成软件的设计和架构。

6.2 存在的问题

  “在编码之前先完成架构”这种做法最大的问题在于,它假设了软件的需求可以预先充分理解。但经验显示,这个假设很多时候甚至可以说大多数时候是不切实际的。只有真正使用了软件、看到了软件对工作的影响,人们才会想明白自己到底需要什么。

6.3 通过重构改善架构

  重构改变了这种观点。有了重构技术,即便是已经在生产环境中运行了多年的软件,我们也有能力大幅度修改其架构。

6.4 应对未来需求变更

  有了重构技术,我就可以采取不同的策略。与其猜测未来需要哪些灵活性、需要什么机制来提供灵活性,我更愿意只根据当前的需求来构造软件,同时把软件的设计质量做得很高。随着对用户需求的理解加深,我会对架构进行重构,使其能够应对新的需要。

7. 重构与软件开发过程

7.1 徒有其名的敏捷开发

  重构起初是作为极限编程,极限编程是最早的敏捷软件开发方法之一。
  如今已经有很多项目使用敏捷方法,但实际上大部分“敏捷”项目只是徒有其名。要真正以敏捷的方式运作项目,团队成员必须在重构上有能力、有热情,他们采用的开发过程必须与常规的、持续的重构相匹配。

7.2 重构和软件开发过程

  三大实践:自测试代码、持续集成、重构

7.2.1 自测试代码

重构的第一块基石是自测试代码。我应该有一套自动化的测试,我可以频繁地运行它们,并且我有信心:如果我在编程过程中犯了任何错误,会有测试失败。

7.2.2 持续集成

  通过持续集成(CI)每个成员的重构都能快速分享给其他同事,不会发生这边在调用一个接口那边却已把这个接口删掉的情况;如果一次重构会影响别人的工作,我们很快就会知道。

7.3 真正的敏捷开发

  有这三大核心实践打下的基础,才谈得上运用敏捷思想的其他部分。持续交付确保软件始终处于可发布的状态,很多互联网团队能做到一天多次发布,靠的正是持续交付的威力。即便我们不需要如此频繁的发布,持续集成也能帮我们降低风险,并使我们做到根据业务需要随时安排发布,而不受技术的局限。有了可靠的技术根基,我们能够极大地压缩“从好点子到生产代码”的周期时间,从而更好地服务客户。这些技术实践也会增加软件的可靠性,减少耗费在bug上的时间。

8. 重构与性能

  重构可能使软件运行更慢,但它也使软件的性能优化更容易。除了对性能有严格要求的实时系统,其他任何情况下“编写快速软件”的秘密就是:先写出可调优的软件,然后调优它以求获得足够的速度。

第三章:代码的坏味道

1. 神秘命名(Mysterious Name)

  描述:包含那些随意的abc、汉语拼音或者不能准确反应含义的名字,总之一切我们看不懂的、烂的都算,好的名字能节省未来用在猜谜上的大把时间。
  重构:改变函数声明、变量改名、字段改名。

2. 重复代码(Duplicated Code)

  描述:在一个以上的地点看到相同的代码结构。
  重构:提炼函数、移动语句、函数上移等手法。

3. 过长函数(Long Function)

  描述:函数越长,就越难理解。需要遵循的原则:每当感觉需要以注释来说明点什么的时候,我们就把需要说明的东西写进一个独立函数中,并以其用途(而非实现手法)命名。我们可以对一组甚至短短一行代码做这件事。哪怕替换后的函数调用动作比函数自身还长,只要函数名称能够解释其用途,我们也该毫不犹豫地那么做。关键不在于函数的长度,而在于函数“做什么”和“如何做”之间的语义距离。
  重构:提炼函数(常用)、以查询取代临时变量、引入参数对象、保持对象完整性、以命令取代参数(消除一些参数)、分解条件表达式、以多态取代条件表达式(应对分支语句)、拆分循环(应对一个循环做了很多事情)。

4. 过长参数列表(Long ParameterList)

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

5. 全局数据(Global Data)

  描述:有少量的全局数据或许无妨,但数量越多,处理的难度就会指数上升。即便只是少量的数据,我们也愿意将它封装起来,这是在软件演进过程中应对变化的关键所在。
  重构:封装变量

6. 可变数据(Mutable Data)

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

7. 发散式变化(Divergent Change)

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

8. 霰弹式修改(Shotgun Surgery)

  描述:霰弹式修改类似于发散式变化,但又恰恰相反。如果每遇到某种变化,你都必须在许多不同的类内做出许多小修改,你所面临的坏味道就是霰弹式修改。如果需要修改的代码散布四处,你不但很难找到它们,也很容易错过某个重要的修改。
  重构:搬移函数、搬移字段、函数组合成类、函数组合成变换、拆分阶段、内联函数、内联字段。

9. 依恋情结(Feature Envy)

  描述:一个函数跟另一个模块中的函数或者数据交流格外频繁,远胜于在自己所处模块内部的交流。也就是违反了高内聚低耦合,处理方法是将这个函数和这个数据摆在一起。一个函数往往会用到几个模块的功能,处置的原则是:判断哪个模块拥有的此函数使用的数据最多,然后就把这个函数和那些数据摆在一起。
  重构:搬移函数、提炼函数。

10. 数据泥团(Data Clumps)

  描述:代码中我们可能经常看到三四个相同的数据:两个类中相同的字段、许多函数签名中相同的参数。这些总是绑在一起出现的数据真应该拥有属于它们自己的对象。
  重构:提炼类、引入参数对象、保持对象完整性。

11. 基本类型偏执(PrimitiveObsession)

  描述:一些基本类型无法表示一个数据的真实意义,例如如钱、坐标、范围等。
  重构:以对象取代基本类型、以子类取代类型码、以多态取代条件表达式。

12. 重复的switch (RepeatedSwitches)

  描述:重复的 switch :在不同的地方反复使用同样的switch 逻辑(可能是以 switch/case 语句的形式,也可能是以连续的 if/else 语句的形式)。重复的 switch 的问题在于:每当你想增加一个选择分支时,必须找到所有的 switch ,并逐一更新。多态给了我们对抗这种黑暗力量的武器,使我们得到更优雅的代码库。
  重构:多态取代条件表达式。

13. 循环语句(Loops)

  描述:早期循环就一直是程序设计的核心要素,在java语言中现在我们可以使用以管道取代循环。
  重构:用管道来取代循环(管道:map、forEach、reduce、filter等一系列)。

14. 冗赘的元素(Lazy Element)

  描述:程序元素(如类和函数)能给代码增加结构,从而支持变化、促进复用或者哪怕只是提供更好的名字也好,但有时我们真的不需要这层额外的结构。对于不需要的程序元素,请让他庄严赴义吧!
  重构:内联函数、内联类、折叠继承类。

15. 夸夸其谈通用性(SpeculativeGenerality)

  描述:为了将来某种需求而实现的某些特殊的处理,这么做的结果往往造成系统更难理解和维护。如果用不上就不需要这样处理,将其删除。
  重构:折叠继承体系、内联函数、内联类、改变函数声明、移除死代码。

16. 临时字段(Temporary Field)

  描述:类中某个字段仅为某种特定情况而设,不需要将其定义为类的成员变量。
  重构:提炼类、提炼函数、引入特例。

17. 过长的消息链(MessageChains)

  描述:一个对象请求另一个对象,然后再向后者请求另一个对象,然后再请求另一个对象……这就是消息链。采取这种方式,意味客户端代码将与查找过程中的导航结构紧密耦合。一旦对象间的关系发生任何变化,客户端就不得不做出相应修改。处理方法:先观察消息链最终得到的对象是用来干什么的,看看能否以提炼函数把使用该对象的代码提炼到一个独立的函数中,再运用搬移函数把这个函数推入消息链。如果还有许多客户端代码需要访问链上的其他对象,同样添加一个函数来完成此事。
  重构: 隐藏委托关系、提炼函数、搬移函数。

18. 中间人(Middle Man)

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

19. 内幕交易(Insider Trading)

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

20. 过大的类(Large Class)

  描述:类内如果有太多代码,也是代码重复、混乱并最终走向死亡的源头。最简单的解决方案是把多余的东西消弭于类内部。
  重构:提炼超类、以子类取代类型码。

21. 异曲同工的类(AlternativeClasses with DifferentInterfaces)

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

22. 纯数据类(Data Class)

  描述:它们拥有一些字段,以及用于访问(读写)这些字段的函数,除此之外一无长物。一般这样的类往往被其他类频繁的调用,这样的类往往是我们没有把调用的行为封装进来,将行为封装进来这种情况就能得到很大改善。
  重构:封装记录、移除取值函数、搬移函数、提炼函数、拆分阶段。

23. 被拒绝的遗赠(RefusedBequest)

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

24. 注释(Comments)

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

第四章:构筑测试体系

  重构是很有价值的工具,但只有重构还不行。要正确地进行重构,前提是得有一套稳固的测试集合,以帮我发现难以避免的疏漏。即便有工具可以帮我自动完成一些重构,很多重构手法依然需要通过测试集合来保障。
  编写优良的测试程序,可以极大提高我的编程速度,即使不进行重构也一样如此。

第五章:介绍重构名录

重构手法都有如下5个部分:

  • 名称(name):要建造一个重构词汇表。
    速写(sketch):帮助你更快找到你所需要的重构手法。
  • 动机(motivation):介绍“为什么需要做这个重构”和“什么情况下不该做这个重构”。
  • 做法(mechanics):简明扼要地一步一步介绍如何进行此重构。
  • 范例(examples):以一个十分简单的例子说明此重构手法如何运作。

第六章:第一组重构

  如果说上面的味道是核心的话,那手法应该就是本书的重中之重。通常我们发现哪里味道不对之后,就要选择使用不同的手法进行重构。将他们变得味道好起来。
  本文中每个手法通常包含三个模块:时机(遇到什么情况下使用)、做法(详细步骤的概括)、关键字(做法的缩影)。

1. 提炼函数(Extract Function)

学新通

  • 曾用名:提炼函数(Extract Method)

  • 反向重构:内联函数

  • 时机:按照“将意图与实现分开”的原则,如果你需要花时间浏览一段代码才能弄清它到底在干什么,那么就应该将其提炼到一个函数中,并根据它所做的事为其命名。以后再读到这段代码时,你一眼就能看到函数的用途,大多数时候根本不需要关心函数如何达成其用途(这是函数体内干的事)。

  • 做法:

  1. 创造一个新函数,根据这个函数的意图来对它命名(以它“做什么”来命名,而不是以它“怎样做”命名)。
  2. 将待提炼的代码从源函数复制到新建的目标函数中。
  3. 仔细检查提炼出的代码,看看其中是否引用了作用域限于源函数、在提炼出的新函数中访问不到的变量。 若是,以参数的形式将它们传递给新函数。
  4. 所有变量都处理完之后,编译。
    在源函数中,将被提炼代码段替换为对目标函数的调用。
  5. 测试。
  6. 查看其他代码是否有与被提炼的代码段相同或相似之处。如果有,考虑使用以函数调用取代内联代码令其调用提炼出的新函数。
  • 关键字:新函数、拷贝、检查、作用域/上下文、编译、替换、修改细节。

作者的一个观点:
  一旦接受了“将意图与实现分开”这个原则,会逐渐养成一个习惯:写非常小的函数——通常只有几行的长度。
  小函数得有个好名字才行,所以你必须在命名上花心思。起好名字需要练习,不过一旦你掌握了其中的技巧,就能写出很有自描述性的代码。

2. 内联函数(Inline Function)

学新通

  • 曾用名:内联函数(Inline Method)

  • 反向重构:提炼函数

  • 时机:

  1. 内部代码和函数名称同样清晰易读。
  2. 手上有一群组织不甚合理的函数。可以先内联到一个大型函数中,再通过提炼函数合理重构。
  3. 代码中有太多简单委托的间接层。通过内联手法,去掉无用的间接层。
  • 做法:
  1. 检查函数,确定它不具多态性。
  2. 找出这个函数的所有调用点。
  3. 将这个函数的所有调用点都替换为函数本体。
  4. 每次替换之后,执行测试。
  5. 删除该函数的定义。
  • 关键字:检查多态、找调用并替换、删除定义。

3. 提炼变量(Extract Variable)

学新通

  • 曾用名:引入解释性变量(Introduce ExplainingVariable)

  • 反向重构:内联变量

  • 时机:

  1. 表达式非常复杂而难以阅读。
  2. 在多处地方使用这个值(可能是当前函数、当前类乃至于更大的如全局作用域)。
  • 做法:
  1. 确认要提炼的表达式没有副作用。
  2. 声明一个不可修改的变量,把你想要提炼的表达式复制一份,以该表达式的结果值给这个变量赋值。
  3. 用这个新变量取代原来的表达式。
  4. 测试。
  • 关键字:副作用、不可修改的变量、赋值、替换。

4. 内联变量(Inline Variable)

学新通

  • 曾用名:内联临时变量(Inline Temp)

  • 反向重构:提炼变量

  • 时机:

  1. 变量能给表达式提供有意义的名字,但这个名字并不比表达式本身更具表现力。
  2. 变量妨碍了重构附近的代码。
  • 做法:
  1. 检查确认变量赋值语句的右侧表达式没有副作用。
  2. 如果变量没有被声明为不可修改,先将其变为不可修改,并执行测试。
  3. 找到第一处使用该变量的地方,将其替换为直接使用赋值语句的右侧表达式。
  4. 测试。
  5. 重复前面两步,逐一替换其他所有使用该变量的地方。
  6. 删除该变量的声明点和赋值语句。
  7. 测试。
  • 关键字:副作用、只读、替换变量。

5. 改变函数声明(Change FunctionDeclaration)

学新通

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

  • 别名:函数改名(Rename Function)

  • 曾用名:函数改名(Rename Method)

  • 曾用名:添加参数(Add Parameter)

  • 曾用名:移除参数(Remove Parameter)

  • 别名:修改签名(Change Signature)

  • 时机:

  1. 函数名字不够贴切函数所做的事情。
  2. 函数参数增加。
  3. 函数参数减少。
  4. 函数参数概念发生变化。
  5. 函数因为某个参数导致的函数应用范围小(全局有很多类似的函数,在做着类似的事情)。
  • 做法:
    有两套:
  1. “简单的做法”:使用时机,变更的范围,能一步到位地修改函数声明及其所有调用者。
  2. 剩余情况用渐进的方式逐步迁移的做法。
  • 简单的做法(改变函数声明):
  1. 如果想要移除一个参数,需要先确定函数体内没有使用该参数。
  2. 修改函数声明,使其成为你期望的状态。
  3. 找出所有使用旧的函数声明的地方,将它们改为使用新的函数声明。
  4. 测试。
  • 迁移式做法:
  1. 如果有必要的话,先对函数体内部加以重构,使后面的提炼步骤易于开展。
  2. 使用提炼函数将函数体提炼成一个新函数。
  3. 如果提炼出的函数需要新增参数,用前面的简单做法添加即可。
  4. 测试。
  5. 对旧函数使用内联函数。
  6. 如果新函数使用了临时的名字,再次使用改变函数声明将其改回原来的名字。
  7. 测试。
  • 关键字:使用变量者、函数调用者、修改函数、声明改名、调用替换。

6. 封装变量(EncapsulateVariable)

  • 曾用名:自封装字段(Self-Encapsulate Field)

  • 曾用名:封装字段(Encapsulate Field)
    学新通

  • 时机:

  1. 当我们在修改或者增加使用可变数据的时候。
  2. 数据被大范围使用(设置值)。
  3. 对象、数组无外部变动需要内部一起改变的需求时候,最好返回一份副本。
  • 做法
  1. 创建封装函数,在其中访问和更新变量值。
  2. 执行静态检查。
  3. 逐一修改使用该变量的代码,将其改为调用合适的封装函数。每次替换之后,执行测试。
  4. 限制变量的可见性。
  5. 测试。
  6. 如果变量的值是一个记录,考虑使用封装记录。

关于做法4——“限制变量的可见性”:
  有时没办法阻止直接访问变量。若果真如此,可以试试将变量改名,再执行测试,找出仍在直接使用该变量的代码。

  • 关键字:新函数、替换调用、不可见。

7. 变量改名(Rename Variable)学新通

  • 动机
  1. 变量/常量的名字不足以说明字段的意义。
  • 做法:
  1. 如果变量被广泛使用,考虑运用封装变量将其封装起来。
  2. 找出所有使用该变量的代码,逐一修改。
  3. 测试。

注意点:

  1. 如果在另一个代码库中使用了该变量,这就是一个“已发布变量”(published variable),此时不能进行这个重构。
  2. 如果变量值从不修改,可以将其复制到一个新名字之下,然后逐一修改使用代码,每次修改后执行测试。
  • 关键字:封装变量手法、替换名字、中间过渡

8. 引入参数对象(IntroduceParameter Object)

学新通

  • 时机:
  1. 一组数据项总是结伴同行。可以用一个数据结构代替。
  2. 函数参数过多。
  • 做法:
  1. 如果暂时还没有一个合适的数据结构,就创建一个。
  2. 测试。
  3. 使用改变函数声明给原来的函数新增一个参数,类型是新建的数据结构。
  4. 测试。
  5. 调整所有调用者,传入新数据结构的适当实例。每修改一处,执行测试。
  6. 用新数据结构中的每项元素,逐一取代参数列表中与之对应的参数项,然后删除原来的参数。测试。
  • 关键字:新结构、增加参数、入参新结构、删除旧参数、使用新结构。

9. 函数组合成类(CombineFunctions into Class)

学新通

  • 时机:
  1. 一组函数形影不离地操作同一块数据(通常是将这块数据作为参数传递给函数)。是时候组建一个类了,类能明确地给这些函数提供一个共用的环境,在对象内部调用这些函数可以少传许多参数,从而简化函数调用,并且这样一个对象也可以更方便地传递给系统的其他部分。
  • 做法:
  1. 运用封装记录对多个函数共用的数据记录加以封装。
  2. 对于使用该记录结构的每个函数,运用搬移函数将其移入新类。
  3. 用以处理该数据记录的逻辑可以用提炼函数提炼出来,并移入新类。
  • 关键字:提炼变量、封装成类、移入已有函数、替换调用、移入计算数据。

10. 函数组合成变换(CombineFunctions into Transform)

学新通

  • 时机:通过输入参数,获取到各种派生信息,这些派生数值会在几个不同地方用到,因此这些计算逻辑会在用到派生数据的地方重复。需要将计算派生数据的逻辑收拢到一处,这样始终可以在固定的地方找到和更新这些逻辑,避免到处重复。

有两种方案:

  1. 采用数据变换(transform)函数:这种函数接受源数据作为输入,计算出所有的派生数据,将派生数据以字段形式填入输出数据。有了变换函数,我就始终只需要到变换函数中去检查计算派生数据的逻辑。
  2. 函数组合成变换:先用源数据创建一个类,再把相关的计算逻辑搬移到类中。

作者对这两个方案点评:

  1. 这两个重构手法都很有用,我常会根据代码库中已有的编程风格来选择使用其中哪一个。
  2. 两者有一个重要的区别:
    • 如果代码中会对源数据做更新,那么使用类要好得多;
    • 如果使用变换,派生数据会被存储在新生成的记录中,一旦源数据被修改,我就会遭遇数据不一致。
  1. 我喜欢把函数组合起来的原因之一,是为了避免计算派生数据的逻辑到处重复。从道理上来说,只用提炼函数
    也能避免重复,但孤立存在的函数常常很难找到,只有把函数和它们操作的数据放在一起,用起来才方便。引入变换(或者类)都是为了让相关的逻辑找起来方便。
  • 做法:
  1. 创建一个变换函数,输入参数是需要变换的记录,并直接返回该记录的值。
  2. 挑选一块逻辑,将其主体移入变换函数中,把结果作为字段添加到输出记录中。修改客户端代码,令其使用这个新字段。
  3. 测试。
  4. 针对其他相关的计算逻辑,重复上述步骤。
  • 关键字:变换函数、变换入参、搬移计算逻辑。

11. 拆分阶段(Split Phase)

学新通

  • 时机:
  1. 一段代码在同时处理两件不同的事。
  • 做法:
  1. 将第二阶段的代码提炼成独立的函数。
  2. 测试。
  3. 引入一个中转数据结构,将其作为参数添加到提炼出的新函数的参数列表中。
  4. 测试。
  5. 逐一检查提炼出的“第二阶段函数”的每个参数。如果某个参数被第一阶段用到,就将其移入中转数据结构。每次搬移之后都要执行测试。
  • 关键字:拆分、提炼、独立的函数。

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhgcbjfj
系列文章
更多 icon
同类精品
更多 icon
继续加载