《重构:改善既有代码的设计》摘抄

第1章:重构,第一个示例

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

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

重构技术就是以微小的步伐修改程序。如果你犯下错误,很容易便可发现它。

傻瓜都能写出计算机可以理解的代码。唯有能写出人类容易理解的代码的,才是优秀的程序员。

将代码块提炼为函数

  • 代码块涉及哪些变量
  • 哪些变量会被代码块修改
  • 哪些变量需要被代码块返回

完成提炼函数后,第一件事就是给变量改名,永远将函数的返回值命名为“result”。

只要改名能够提升代码的可读性,那就应该毫不犹豫去做。

优先关注局部变量和临时变量。临时变量往往会带来麻烦,临时变量实质上会鼓励你写长而复杂的函数。

对于重构过程中的性能问题,大多数情况下可以忽略它。如果重构引入了性能损耗,先完成重构,再做性能优化,因为重构可以使用更高效的性能调优方案。

尽量保持数据不可变,可变的状态会很快变成烫手山芋。

言以简为贵,可演化的软件以明确为贵。

编程时,需要遵循营地法则:保证你离开时的代码库一定比来时更健康。

以工厂函数取代构造函数。

以多态取代条件表达式。

如果大多数修改都涉及特定类型的计算,那么按类型进行分离就很有意义。有越多的函数依赖于同一套类型进行多态,那么这种继承方案就越有益处。

本章重构的三个重点

  • 将原函数分解成一组嵌套的函数
  • 应用拆分阶段分离计算逻辑与输出格式化逻辑
  • 为计算器引入多态性来处理计算逻辑

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

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

第2章 重构的原则

重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。

重构(动词):使用一系列重构首发,在不改变软件可观察行为的前提下,调整其结构。

如果有人说他们的代码在重构过程中有一两天时间不可用,基本上可以确定,他们在做的事不是重构。

重构和性能优化的区别

  • 重构是为了让代码“更容易理解”,更易于修改,这可能使程序运行得更快,也可能是程序运行得更慢。
  • 性能优化只关心让程序运行得更快,最终得到的代码有可能更难理解和维护,对此要有心理准备。

两顶帽子

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

为何重构

  • 重构改进软件的设计
  • 重构使软件更容易理解
  • 重构帮助找到bug
  • 重构提高编程速度

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

何时重构

  • 预备性重构:让添加新功能更容易
  • 帮助理解的重构:使代码更易懂
  • 捡垃圾式重构
  • 有计划的重构和见机行事的重构
  • 长期重构
  • 复审代码时重构

肮脏的代码必须重构,但漂亮的代码也需要很多重构。

何时不该重构:一堆凌乱的代码,但并不需要修改它,就不需要重构。

重构的唯一目的就是让我们开发更快,用更少的工作量创造更大的价值。重构的意义不在于把代码库打磨得闪闪发亮,而是纯粹经济角度出发的考量。

测试的目的:快速发现错误。

与其猜测未来需要哪些灵活性,需要什么机制来提供灵活性,我更愿意只根据当前的需求来构造软件,同时把软件的设计质量做得很高。要判断是否应该为未来的变化添加灵活性,我会评估“如果以后再重构有多困难”,只有当未来重构会很困难时,我才考虑现在就添加灵活性机制。

编写快速软件的方法

  • 时间预算法:每个模块的时间/空间资源是预先分配的;
  • 持续关注法:任何程序员在任何时候做任何事,都要保持系统的高性能;
  • 先构造良好的程序,再进行性能优化

第3章:代码的坏味道

解释“如何删除一个实例变量”或“如何产生一个继承体系”很容易,因为这些都是简单的市顷,但是要解释“该在什么时候做这些动作”就没那么顺理成章了。

观察代码时,我们从中寻找某些特定结构,这些结构指出重构的可能性。

从我们的经验来看,没有任何量度规矩比得上见识广博者的直觉。你必须培养自己的判断力,学会判断一个类内有多少实例变量算是太大,一个函数内有多少行代码才算太长。

神秘命名

  • 很多人敬仰不愿意给程序元素改名,觉得不值得费这个劲,但好的名字能节省未来用在猜谜上的大把时间。
  • 重构手法:改变函数声明,变量改名,字段改名

重复代码

  • 重构手法:提炼函数,移动语句,函数上移

过长函数

  • 在早期的编程语言中,子程序调用需要额外开销,这使得人们不乐意使用小函数。现代编程语言几乎已经完全免除了进程内的函数调用开销。
  • 每当感觉需要以注释来说明点什么的时候,我们就把需要说明的东西写进一个独立的函数中,并以其用途(而非实现手法)命名。
  • 如何确定该提炼哪一段代码呢?一个很好的技巧是:寻找注释。
  • 注释通常能指出代码用途和实现手法之间的语义距离。
  • 重构手法:提炼函数,以查询取代临时变量,引入参数对象,保持对象完整,以命令取代函数,分解条件表达式,以多态取代条件表达式,拆分循环

过长参数列表

  • 重构手法:以查询取代参数,保持对象完整,引入参数对象,移除标记参数,函数组合成类

全局数据

  • 全局数据最显而易见的形式就是全局变量,但类变量和单例也有这样的问题。
  • 重构手法:封装变量,搬移变量

可变数据

  • 函数式编程完全建立在“数据永不改变”的概念基础上:如果要更新一个数据结构,就返回一份新的数据副本,旧的数据仍保持不变。
  • 如果可变数据的值能在其他地方计算出来,这就是一个特别刺鼻的坏味道。
  • 重构手法:封装变量,拆分变量,移动语句,提炼函数,将查询函数和修改函数分离,移除设置函数,以查询取代派生变量,函数组合成类,函数组合成变换,将引用对象改为值对象。

发散式变化

  • 我们希望软件能够更容易被修改——毕竟软件本来就该是“软”的。
  • 当你看着一个类说“呃,如果新加入一个数据库,我必须修改这3个函数;如果新出现一种金融工具,我必须修改这4个函数。”这就是发散式变化的征兆。
  • 每当要对某个上下文做修改时,我们只需要理解这个上下文,而不必操心另一个。
  • 重构手法:拆分阶段,搬移函数,提炼函数,提炼类

霰弹式修改

  • 如果每遇到某种变化,你都必须在许多不同的类内作出许多小修改,你所面临的坏味道就是霰弹式修改。
  • 重构手法:搬移函数,搬移字段,函数组合成类,函数组合成变换,拆分阶段,内联函数,内联类

依恋情结

  • 一个函数跟另一个模块中的函数或者数据交流格外频发,远胜于在自己所处模块内部的交流,这就是依恋情结的典型情况。
  • 一个函数往往会用到几个模块的功能,那么它究竟该被置于何处?我们的原则是:判断哪个模块拥有的此函数使用的数据最多,然后就把这个函数和那些数据摆在一起。
  • 最根本的原则:将总是一起变化的东西放在一块。
  • 重构手法:搬移函数

数据泥团

  • 你常常可以在很多地方看到相同的三四项数据:两个类中相同的字段,许多函数签名中相同的参数。
  • 一个好的评判办法是:删掉众多数据中的一项。如果这么做,其他数据有没有因而失去意义?如果它们不再有意义,这就是一个明确信号:你应该为它们产生一个新的对象。
  • 重构手法:提炼类,引入参数对象,保持对象完整

基本类型偏执

  • 重构手法:以对象取代基本类型,以子类取代类型码,以多态取代条件表达式,提炼类,引入参数对象

重复的switch

  • 在不同的地方反复使用同样的swtich逻辑。
  • 重构手法:以多态取代条件表达式。

循环语句

  • 重构手法:以管道取代循环

冗赘的元素

  • 重构手法:内联函数,内联类,折叠继承体系

夸夸其谈通用性

  • 重构手法:折叠继承体系,移除死代码,内联函数,内联类,改变函数声明

临时字段

  • 类内部某个字段仅为某种特定情况而设。这样的代码让人不易理解,因为你通常认为对象在所有时候都需要它的所有字段。在字段未被使用的情况下猜测当初设置它的目的,会让你发疯。
  • 重构手法:提炼类,搬移函数,引入特例

过长的消息链

  • 重构手法:隐藏委托关系,提炼函数,搬移函数

中间人

  • 对象的基本特征之一就是封装——对外部世界隐藏其内部细节。
  • 重构手法:移除中间人,内联函数,以委托取代超类,以委托取代子类。

内幕交易

  • 软件开发者喜欢在模块之间建起高墙,极其反感在模块之间大量交换数据,因为这会增加模块间的耦合。在实际情况里,一定的数据交换不可避免,但我们必须尽量减少这种情况,并把这种交换都放在明面上来。
  • 重构手法:搬移函数,搬移字段,隐藏委托关系,以委托取代子类,以委托取代超类

过大的类

  • 如果有5个“百行函数”,它们之中很多代码相同,那么或许你可以把它们变成5个“十行函数”和10个提炼出来的“双行函数”
  • 重构手法:提炼超类,以子类取代类型码,提炼类

异曲同工的类

  • 重构手法:改变函数声明,搬移函数,提炼超类

纯数据类

  • 纯数据类常常意味着行为被放在了错误的地方。也就是说,只要把处理数据的行为从客户端搬移到纯数据类里来,就能使情况大为改观。
  • 重构手法:封装记录,移除设置值函数,拆分阶段,搬移函数,提炼函数

被拒绝的遗赠

  • 拒绝继承超类的实现,这一点我们不介意;但如果拒接支持超类的接口,这就难以接受了。既然不愿意支持超类的接口,就不要虚情假意的糊弄继承体系。
  • 重构手法:函数下移,字段下移,以委托取代子类,以委托取代超类

注释

  • 当你感觉需要撰写注释时,请先尝试重构,试着让所有注释都变得多余
  • 如果你不知道该做什么,这才是注释的良好运用时机。除了用来记述将来的打算之外,注释还可以用来标记你并无十足把握的区域。你可以在注释里写下“为什么做某某事”。这类信息可以帮助将来的修改者,尤其是那些健忘的家伙。
  • 重构手法:提炼函数,改变函数声明,引入断言。

第4章 构筑测试体系

要正确地进行重构,前提是得有一套稳固的测试集合,以帮我发现难以避免的疏漏。

确保所有测试都完全自动化,让它们检查自己的测试结果。

一套测试就是一个强大的bug侦测器,能够大大缩减查找bug所需的时间。

很多人根本没学过如何编写测试程序,甚至根本没考虑过测试,这对于编写自测试也很不利。

编写测试代码还能帮我把注意力集中于接口而非实现。

测试驱动开发依赖这个短循环:先编写一个(失败的)测试,编写代码使测试通过,然后进行重构以保证代码整洁。

少许测试往往就足以带来惊人的收益。

总是确保测试不该通过时真的会失败。

频繁地运行测试。对于你正在处理的代码,与其对应的测试至少每隔几分钟要运行一次,每天至少运行一次所有的测试。

观察被测试类应该做的所有事情,然后对这个类的每个行为进行测试,包括各种可能使它发生异常的边界条件。

我不会去测试那些仅读或写一个字段的访问函数,因为它们太简单了,不太可能出错。

共享测试夹具会使测试间产生交互,这是滋生bug的温床。

配置——检查——验证——(拆除)

准备——行为——断言——(拆除)

happy path:正常路径,指的是一切工作正常,用户使用方式也最符合规范的那种场景。

考虑可能出错的边界条件,把测试火力集中在那。

扮演“程序公敌”的角色,积极思考如何破坏代码。

如果试图编写太多测试,你也可能因为工作量太大而气馁,最后什么都写不成。你应该把测试集中在可能出错的地方。观察代码,看哪儿变得复杂;观察函数,思考哪些地方可能出错。

之前,测试更多被认为是另一个独立的团队的责任,但现在它愈发成为任何一个软件开发者所必备的技能。

每当你遇到一个bug,先写一个测试来清楚的复现它。

一个测试集是否足够好,最好的衡量标准其实是主观的,请你问自己:如果有人在代码里加入了一个缺陷,你有多大的自信它能够被测试集揪出来?这种信心难以被定量分析,但自测试代码的全部目标,就是要帮你获得这种信心。如果我重构完成代码,看见全部变绿的测试就可以十分自信没有引入额外的bug,这样,我就可以高兴地说,我已经有一套足够好的测试。

第5章 介绍重构名录

重构的记录格式

  • 名称:如今重构经常会有多个名字,所以我会同时列出常见的别名;
  • 速写:速写的用意不是解释重构的用途和详细步骤,而是如果你曾经看过这个重构手法,速写能帮你回忆它;
  • 动机:为什么需要做这个重构,什么情况下不该做这个重构;
  • 做法:简明扼要的一步一步介绍如何进行重构,但是不会介绍为什么;
  • 范例:通过一个简单的例子说明重构手法如何运作,并介绍为什么;

小步前进,情况越复杂,步子就要越小。

本书中的每个重构,逻辑上来说,都有一个反向重构。

第6章 第一组重构

提炼函数

  • 将意图与实现分开:如果你需要花时间浏览一段代码才能弄清它到底在干什么,那么就应该将其提炼到一个函数中,并根据它所做的是为其命名
  • 在最简单的情况下,无局部变量,提炼函数易如反掌
  • 局部变量最简单的情况是,被提炼单吗只是读取这些变量的值,并不修改它们
  • 如果被提炼的代码对局部变量赋值,问题就变得复杂了 P112

内联函数

  • 本书经常以间断的函数表现动作意图,这样会使代码更清晰易读。但有时候你会遇到某些函数,其内部代码和函数名称同样清晰易读。也可能你重构了该函数的内部实现,使其内容和其名称变得同样清晰。若果真如此,你就应该去掉这个函数,直接使用其中的代码。
  • 间接性可能带来帮助,但非必要的间接性总是让人不舒服。
  • 我手上有一群组织不甚合理的函数,可以将它们都内联到一个大函数中,然后再提炼小函数。
  • 如果代码中有太多间接层,使得系统中的所有函数都似乎只是对一个函数的简单委托,造成我在这些委托动作之间晕头转向,那么我通常会使用内联函数。

提炼变量

  • 引入解释性变量,它们给调试器和打印语句提供了便利的抓手

内联变量

  • 有时候变量并不比表达式本身更有表现力
  • 还有时候变量可能会妨碍重构附近的代码

改变函数声明

  • 邪恶的混乱魔王总是这样引诱我:就算这个名字有点迷惑人,还是放着别管吧——说到底,不过就是一个名字而已。
  • 先写一句注释描述这个函数的用途,再把这句注释变成函数名字
  • 函数的参数列表阐述了函数如何与外部世界共处
  • 修改参数列表不仅能增加函数的应用范围,还能改变连接一个模块所需的条件,从而去除不必要的耦合
  • 修改调用函数之前,先应用引入断言,确保调用方一定会使用这个新参数,断言会帮助抓到错误。

封装变量

  • 对于所有可变的数据,只要它的作用域超过单个函数,我就会将其封装起来,只允许通过函数访问。
  • 数据的作用域越大,封装就越重要。
  • 封装数据很重要,不可变数据更重要。
  • 封装数据很有价值,但往往并不简单。到底该封装什么,以及如何封装,取决于数据被使用的方式,以及我想要修改数据的方式。不过一言以蔽之,数据被使用的越广,就越值得花精力给它一个体面的封装。

变量改名

  • 我常会把名字起错——有时因为想的不够仔细,有时因为对问题的理解加深了,有时因为程序的用途随着用户的需求改变了。

引入参数对象

函数组合成类

  • 如果发现一组函数形影不离的操作同一块数据(通常是这块数据作为参数传递给函数),我就认为,是时候组建一个类了。
  • 使用类有一大好处:客户端可以修改对象的核心数据,通过计算得出的派生数据则会与核心数据保持一致。
  • 一组函数可以组合成类,也可以组合成嵌套函数。我更倾向于类而非嵌套函数,因为嵌套函数测试起来很困难。如果我想对外暴露多个函数,也必须采用类的形式。
  • 如果数据确有可能会被更新,那么用类将其封装起来会很有帮助。

函数组合成变换

  • 函数组合成变换和函数组合成类的一个重要区别:如果代码中会对源数据做更新,那么使用类要好得多;如果使用变换,派生数据会被存储在新生成的记录中,一旦源数据被修改,就会遭遇数据不一致。
  • 如果一个变换函数本质上仍是原来的对象,只是添加了更多的信息,我喜欢用enrich来命名。如果它生成的是跟原来完全不同的对象,我就会用transform来命名。

拆分阶段

  • 最简洁的拆分方法之一,就是把一大段行为分成顺序执行的两个阶段;
  • 先将第二阶段提炼成独立函数;
  • 引入中专数据结构;
  • 再对第一阶段提炼函数;
  • 把尽可能多的参数搬移到中转数据结构;

第7章 封装

封装记录

  • 简单的记录型结构最恼人的一点:它强迫我区分“记录中存储的数据”和“通过计算得到的数据”
  • 对于可变数据,我更偏爱使用类对象而非记录的原因;
  • 对于不可变数据,可以直接将数据保存在记录里,需要做数据变换的时候增加一个填充步骤即可。
  • 如果记录比较复杂,例如是个嵌套结构,那么先重点关注客户端对数据的更新操作,对于读取操作可以考虑返回一个数据副本或只读的数据代理。
  • 最重要的是妥善处理好更新操作。
  • 封装大型数据结构时,凸显更新操作,将它们集中到一处地方,是此次封装过程中最重要的部分。

封装集合

  • 我喜欢封装程序中所有的可变数据,这使得很容易看清楚数据被修改的地方和修改方式,这样我需要更改数据结构时非常方便。
  • 封装集合时人们常犯的一个错误:只对集合变量的访问做了封装,但依然让取值函数返回集合本身。这使得集合的成员变量可以被直接修改,而封装它的类全然不知,无法介入。
  • 一种避免直接修改集合的方法是,永远不直接返回集合的值;
  • 另一种方法是,以某种形式限制集合的访问权,只允许对集合进行读操作;
  • 无论采用哪种方法,最重要的是在同一套代码库中保持一致的做法。

以对象取代基本类型

  • 一旦我发现对某个数据的操作不仅局限于打印,我就为它创建一个新的类。一开始这个类也许只是简单的包装一下简单类型的数据,不过只要类有了,日后添加业务逻辑就有地可去了。

以查询取代临时变量

  • 将变量的计算逻辑放到函数中,也有助于提炼得到的函数与原函数之间设立清晰的边界,这能帮我发现并避免难缠的依赖及副作用。
  • 这项手法在类中施展效果最好,因为类为待提炼函数提供了一个共同的上下文。如果不是在类中,我很可能会在顶层函数中拥有多个参数,这将冲淡提炼函数所带来的好处。使用嵌套的小函数可以避免这个问题,但又限制了我在相关函数间分享逻辑的能力。
  • 该手法只适用于处理某些类型的临时变量:那些只被计算一次而且之后不再被修改的变量。

提炼类

  • 一个类应该是一个清晰的抽象,只处理一些明确的责任,但实际工作中,类会不断成长扩展。随着责任不断增加,类会变得过分复杂。
  • 先搬移较低层函数,也就是“被其他函数调用”多于“调用其他函数”的函数。

内联类

  • 如果一个类不再承担足够的责任,挑选一个这个类的最频繁用户类,然后将这个萎缩的类塞进用户类

隐藏委托关系

  • 一个好的模块化涉及,“封装”是最关键特征之一。“封装”意味着每个模块都应该尽可能少了解系统其他部分。如此一来,一旦发生变化,需要了解这一变化的模块就会比较少——这会使得变化比较容易进行。
  • 如果某些客户端先通过服务对象的字段得到另一个对象(受托类),然后调用后者函数,那么客户就必须知晓这一层委托关系。

移除中间人

  • 隐藏委托关系的代价是,每当客户端使用受托类的新特性时,你就必须在服务端添加一个简单的委托函数。随着受托类的特性越来越多,更多的转发函数就会使人烦躁。服务类完全变成了一个中间人,此时就应该让客户直接调用受托类。
  • 重构的意义在于:你永远不必说对不起——只要把出问题的地方修不好就行了。

替换算法

  • “重构”可以把一些复杂的东西分解为简单的小块,但有时必须壮士断腕,删掉整个算法,代之以较简单的算法。
  • 替换一个巨大复杂的算法是非常困难的,只有先将它分解为较简单的小型函数,才能有把握进行算法的替换工作。
posted @ 2020-03-28 17:33  ZH奶酪  阅读(411)  评论(0编辑  收藏  举报