前言
如果你要给程序添加一个特性,但发现代码因缺乏良好的结构而不易于进行更改,那就先重构那个程序,使其比较容易添加该特性,然后再添加该特性。
需求的变化使得重构变得必要。如果一段代码能正常工作并且不会再被修改,那么完全可以不去重构它。
1、重构第一步
- 确保即将修改的代码拥有一组可靠的测试。
- 小步修改,每次修改后就运行测试,在犯错时只需要考虑一个很小的改动范围。这会使得查错与修复问题易于反掌。
好代码的检验标准:
有人需要修改代码时,他们应能轻易找到修改点,应该能快速做出更改而不易引入其他错误。
一个健康的代码库能够最大限度地提升我们的生产力,支持我们更快、更低成本的为用户添加新特性。为了保持代码库的健康,就需要时刻留意现状与理想之间的差距,然后通过重构不断接近这个理想。
2、重构的原则
重构与性能优化的异同点:
同:
- 两者都需要修改代码,并且两者都不会改变程序的整体性能。
异:
- 重构是为了让代码”更容易理解,更易于修改“,这可能使得程序运行的更快,也可能使程序运行的更慢
- 性能优化时,只关心让程序运行的更快,最终得到的代码有可能更难理解和维护。
2.1 两顶帽子
Kent Beck 提出了”两顶帽子“的比喻:添加新功能和重构。
- 添加新功能是,不应该修改既有代码,只管添加新功能。通过添加测试并让测试正常运行,我可以衡量自己的工作进度。
- 重构是就不能再添加新功能,只管调整代码的结构。此时不应该添加任何测试(除非发现先前有遗漏的东西), 只在绝对必要(用以处理接口变化)时才修改测试。
2.2 为何重构
重构改进软件的设计
- 如果没有重构,程序的内部设计(或者叫架构)会逐渐腐败变质。当人们只为短期目的而修改代码时, 他们经常没有完全理解架构的整体设计,于是代码逐渐失去了自己的结构。改进设计的一个重要方向就是消除重复代码
- 重构使软件更容易理解
- 重构帮助找到bug
- 重构提高编程速度
2.3 何时重构
三次法则:
- 第一次做某件事时只管去做
- 第二次做类似的事会产生反感,但无论如何还是可以去做
- 第三次再做类似的事,就应该重构
预备性重构:让添加新功能更容易
- 重构的最假时机就在添加新功能之前。
帮助理解的重构:使代码更易懂
- 如果一段代码的条件逻辑的结构很糟糕,也可能希望复用一个函数。但花费了一段时间才能弄懂他到底在做什么,这些都是重构的机会。
捡垃圾式重构
- 已经理解代码在做什么,但发现它做得不好,例如逻辑不必要地迂回复杂,或者两个函数几乎完全相同,可以用一个参数化的函数取而代之。
有计划的重构和见机行事的重构
长期重构
复审代码时重构
2.4 何时不应该重构
- 如果有一段凌乱的代码,但并不需要修改它,那么就不需要重构。只有需要理解其工作原理时,对其进行重构才有价值。
- 如果重写比重构还容易,就不需重构了。
2.5 重构的挑战
延缓新功能开发
- 这种认知是错误的,重构的唯一目的就是让我们开发更快,用更少的工作量创造更大的价值。
分支
- 每个程序员在自己的分支上开发,可能较长时间才将代码合并到主分支上在 团队内部进行共享。这给重构造成了阻碍。
测试
- 不会改变程序可观察行为,这是重构的一个重要特性。
- 如果想要重构,得先有可以自测试的代码。
遗留代码
- 遗留代码往往很复杂,测试也不足。
数据库
- 数据库是”重构经常出问题的一个领域“。
3、重构与性能
重构可能使软件运行更慢,但它也使软件的性能优化更容易。除了对性能有严格要求的实时系统,其他任何情况下”编写快速软件“的秘密就是:先写出可调优的软件,然后调优它以求获得足够的速度。
快速编写软件的方法:
时间预算法
- 这种方法,分解你的设计时就要做好预算,给每个组件预先分配一定资源,包括时间和空间占用。每个组件绝对不能超出自己的预算,就算拥有组件之间调度预配时间的机制也不行。
持续关注法
- 这种方法要求任何程序员在任何时间做任何事时,都要设法保持系统的高性能21·hi品牌是现代化佳节快乐;托管费的出现 了,
利用百分之九十统计数据。
- 采用这种方法时,编写构造良好的程序,不对性能投以特别的关注,直至进入性能优化阶段——通常是在开发后期。
4、代码的坏味道
4.1 神秘命名
整洁代码最重要的一环就是好的名字,所以我们会深思熟虑如何给函数、模块、变量和类命名,使他们能清晰地表明自己的功能和用法。
4.2 重复代码
如果在一个以上的地方看到相同的代码结构,那么可以肯定:设法将他们合二为一,程序会变得更好。一旦有重复代码存在,阅读这些重复代码时需加倍仔细,留意其间细微的差异。如果修改重复代码,必须找出所有的副本来修改。
4.3 过长函数
函数越长,越难理解。
每当感觉需要以注释来说明点什么的时候,我们就把需要说明的东西写进一个独立函数中,并以其用途(而非实现手法)命名。
4.4 过长参数列表
使用类可以有效地缩短参数列表。
4.5 全局数据
全局数据的问题在于,从代码库的任何一个角落都可以修改它,而且没有任何机制可以探测出到底哪段代码做出了修改。
4.6 可变数据
对数据的修改经常导致出乎意料的结果和难以发现的bug。我在一处更新数据,却没有意识到软件中的另一处期望着完全不同的数据,于是一个功能失效了——如果故障只是在很罕见的情况下发生,要找出故障原因就会更加困难。
4.7 发散式变化
一旦需要修改,只需要找到系统的某一点,只在该处做修改。
4.8 霰弹式修改
每遇到某种变化,必须在许多不同的类内做出许多小修改。如果需要修改的代码散步四处,不但很难找到它们,也很容易错过某个重要的修改。
4.6 此外还有
依恋情结、数据泥团、基本类型偏执、重复的Switch、循环语句、冗赘的元素、夸夸其谈通用性、临时字段、过长的消息链、过度封装、大量交换数据、过大的类、异曲同工的类、纯数据类等
5、构筑测试体系
要正确的进行重构,前提是得有一套稳固的测试集合,以帮助我们发现难以避免的疏漏。
编写优良的测试程序,可以极大提高我们的编程速度,即使不进行重构也一样如此。
频繁的运行测试。对于你正在处理的代码,与其对应的测试至少每隔几分钟就要运行一次,每天至少运行一次所有的测试。
6、介绍重构名录
6.1 重构的记录格式
重构的标准格式:
名称
- 要建造一个重构名词汇,名称很重要
速写
- 可以帮助我们更快找到所需要的重构手法
动机
- 介绍“为什么需要做这个重构”和“什么情况下不该做这个重构”
做法
- 简明扼要地一步一步介绍如何进行重构
范例
- 以一个十分简单的例子说明此重构手法如何运作
6.2 第一组重构
提炼函数、提炼变量、内联函数、内联变量、改变函数声明、封装变量、变量改名、引入参数对象、函数组合成类、函数组合成变换、拆分阶段
6.2.1 提炼函数
曾用名:提炼函数
反向重构:内联函数
动机:
- 如果你需要花时间浏览一段代码才能弄清楚它到底在干什么,那么就应该将其提炼到一个函数中,并根据他所做的事为其命名。
做法:
- 创造一个新函数,根据这个函数的意图来对他命名(以他“做什么”来命名,而不是以他”怎样做“命名)。
- 将待提炼的代码从原函数复制到新建的目标函数中。
- 仔细检查提炼出的代码,看看其中是否引用了作用域限于源函数、在提炼出的新函数中访问不到的变量。若是,以参数的形式将他们传递给新函数。
- 所有变量都处理完之后,编译。
- 在源函数中,将被提炼代码段替换为对目标函数的调用。
- 测试。
- 查看其他代码是否有与被提炼的代码段相同或相似之处。如果有,考虑使用以函数调用取代内联代码令其调用提炼出的心函数。 6.2.2 内联函数
曾用名:内联函数
反向重构:提炼函数
动机:
- 有些函数其内容和其名称同样清晰易读,也可能你重构了该函数的内部实现,使其内容和名称变得同样清晰,那就应该去掉这个函数,直接使用其中的代码。
做法:
- 检查函数,确定它不具多态性。
- 找出这个函数的所有调用点。
- 将这个函数的所有调用点都替换成函数本体。
- 每次替换之后,执行测试。
- 删除该函数的定义。 6.2.3 提炼变量
曾用名:引入解释性变量
反向重构:内联变量
动机:
- 表达式有可能非常复杂而难以阅读。这种情况下,局部变量可以帮助我们将表达式分解为比较容易管理的形式。在面对一块复杂逻辑时,局部变量使我能给其中的一部分命名,这样我就能更好的理解这部分逻辑是要干什么。
做法:
- 确认要提炼的表达式没有副作用。
- 声明一个不可修改的变量,把你想要提炼的表达式复制一份,以该表达式的结果值给这个变量赋值。
- 用这个新变量取代原来的表达式。
- 测试。 6.2.4 内联变量
曾用名:内联临时变量
反向重构:提炼变量
动机:
- 在一个函数内部,变量能给表达式提供有意义的名字,因此通常变量是好东西。但有时候,这个名字并不比表达式本身更具有表现力。还有些时候,变量可能会妨碍重构附近的代码。若果真如此,就应该通过内联的手法消除变量。
做法:
- 检查确认变量赋值语句的右侧表达式没有副作用。
- 如果变量没有被声明为不可修改,先将其变为不可修改,并执行测试。
- 找到第一处使用该变量的地方,将其替换为直接使用赋值语句的右侧表达式。
- 测试。
- 重复前面两步,逐一替换其他所有使用该变量的地方。
- 删除该变量的声明点和赋值语句。
- 测试。 6.2.5 改变函数声明
别名:函数改名
曾用名:函数改名
曾用名:添加参数
曾用名:移除参数
别名:修改签名
动机:
- 一个好的名字能让我们一眼看出函数的用途,而不必查看其实现代码。
做法:
简单的做法
- 如果想要移除一个参数,需要先确定函数体内没有使用该参数。
- 修改函数声明,使其称为你期望的状态。
- 找出所有使用旧的函数声明的地方,将它们改为使用新的函数声明。
- 测试
迁移式做法
- 如果有必要的话,先对函数体内部加以重构,使后面的提炼步骤易于开展。
- 使用提炼函数将函数体提炼成一个新函数。
- 如果提炼出的函数需要新增参数,用前面的简单做法添加即可。
- 测试。
- 对旧函数使用内联函数。
- 如果新函数使用了临时的名字,再次使用改变函数声明将其改回原来的名字。
- 测试。 6.2.6 封装变量
曾用名:自封装字段
曾用名:封装字段
动机:
- 封装能提供一个清晰地观测点,可以由此监控数据的变化和使用情况,还可以轻松的添加数据被修改时的验证或后续逻辑。
做法:
- 创建封装函数,在其中访问和更新变量值。
- 执行静态检查。
- 逐一修改使用该变量的代码,将其改为调用合适的封装函数。每次替换之后,执行测试。
- 限制变量的可见性。
- 测试。
- 如果变量的值是一个记录,考虑使用封装记录。 6.2.7 变量改名
动机:
- 好的命名是整洁编程的核心。如果变量名起得好的话,变量可以很好地解释一段程序在干什么。
机制:
- 如果变量被广泛使用,考虑运用 封装变量将其封装起来。
- 找出所有使用该变量的代码,逐一修改。
- 测试。 6.2.8 引入参数对象
动机:
- 这项重构真正的意义在于,他会催生代码中更深层次的改变。
做法:
- 如果暂时还没有一个合适的数据结构,就创建一个。
- 测试。
- 使用改变函数声明给原来的函数新增一个 参数,类型是新建的数据结构。
- 测试。
- 调整所有调用者,传入新数据结构的适当实例。每修改一处,执行测试。
- 用新数据结构中的每项元素,逐一取代参数列表中与之对应的参数项,然后删除原来的参数。
- 测试。 6.2.9 函数组合成类
动机:
- 如果发现一组函数形影不离的操作同一块数据(通常是将这块数据作为参数传递给函数),我就认为,是时候组建一个类了。类能明确的给这些函数提供一个共用的环境,在对象内部调用这些函数可以少传许多参数,从而简化函数调用,并且这样一个对象也可以更方便的传递给系统的其他部分。
- 除了可以把已有的函数组织起来,这个重构还给我们一个机会,去发现其他的计算逻辑,将它们也重构到新的类当中。
做法:
- 运用封装记录对多个函数共用的数据记录加以封装。
- 对于使用该记录结构的每个函数,运用搬移函数将其移入新类。
- 用以处理该数据记录的逻辑可以用提炼函数提炼出来,并移入新类。 6.2.10 函数组合成变换
动机:
- 避免计算派生数据的逻辑到处重复。
做法:
- 创建一个变换函数,输入参数是需要变换的记录,并直接返回该记录的值。
- 挑选一块逻辑,将其主体移入变换函数中,把结果作为字段添加到输出记录中。
- 修改客户端代码,令其使用这个新字段。
- 测试。
- 针对其他相关的计算逻辑,重复上述步骤。 6.2.11 拆分阶段
动机:
- 每当看见一段代码都在同时处理两件不同的事情,就想把它拆成各自独立的模块,因为这样到了修改的时候,就可以单独处理每个主题,而不必同时在脑子里考虑两个不同的主题。
做法:
- 将诶二阶段的代码提炼成独立的函数。
- 测试。
- 引入一个中转数据结构,将其作为参数添加到提炼出的新函数的参数列表中。
- 测试。
- 逐一检查提炼出的”第二阶段函数“的每个参数。如果某个参数被第一阶段用到,就将其移入中转数据结构。
- 每次搬移之后都要执行测试。
- 对第一阶段的代码运用提炼函数,让提炼出的函数返回中转数据结构。