zoukankan      html  css  js  c++  java
  • 《重构:改善既有代码的设计》摘抄

    第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章 封装

    封装记录

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

    封装集合

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

    以对象取代基本类型

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

    以查询取代临时变量

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

    提炼类

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

    内联类

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

    隐藏委托关系

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

    移除中间人

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

    替换算法

    • “重构”可以把一些复杂的东西分解为简单的小块,但有时必须壮士断腕,删掉整个算法,代之以较简单的算法。
    • 替换一个巨大复杂的算法是非常困难的,只有先将它分解为较简单的小型函数,才能有把握进行算法的替换工作。
  • 相关阅读:
    自定义组件要加@click方法
    绑定样式
    647. Palindromic Substrings
    215. Kth Largest Element in an Array
    448. Find All Numbers Disappeared in an Array
    287. Find the Duplicate Number
    283. Move Zeroes
    234. Palindrome Linked List
    202. Happy Number
    217. Contains Duplicate
  • 原文地址:https://www.cnblogs.com/CheeseZH/p/12588401.html
Copyright © 2011-2022 走看看