读书笔记-《重构改善代码结构》

1,033 阅读18分钟

前言

这本书最早是在一次会议上被提到,我当时并没看过。其实重构在我们写代码的过程时时刻刻都有发生,消除重复代码,提取公共函数,去掉多余的参数,将方法参数封装成对象,或者是优雅的用到设计模式等等。无论是大的改造还是小的改动,你觉得代码虽然功能正常,但就是写的不好,难以理解或者不优雅,不够逼格的时候,重构就开始了。
(这本书是英文翻译过来的,有些地方语句不通,也勉强读完了前三章,把笔记分享出来,不想看书的可以看看笔记,想看书的可以找我要电子版链接。)

重构是什么

所谓重构是这样一个过程:在不改变代码外在行为的前提下,对代码做出修改,以改进程序的内部结构。重构是一种经千锤百炼形成的有条不紊的程序整理方法,可以最大限度的减少整理过程中引入错误的几率。本质上说,重构就是在代码写好之后改进它的设计。

第一章:重构第一个案例

1.1起点

如果你发现自己需要为程序添加一个特性,而代码结构使你无法很方便的达成目的,那就先重构那个程序,使特性的添加比较容易进行,然后再添加特性。

1.2重构的第一步

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

1.3分解并重组statement()

本章重构过程的第一阶段,就是把长长的函数切开,并把较小快的代码移至更合适的类,即找出代码的逻辑泥团并运用Extract Method。 重构结束就是以微小的步伐修改程序。如果你犯下错误,很容易便可发现它。 要改变量名称是值得的行为吗?绝对值得。好的代码应该清楚表达自己的功能,变量名称是代码清晰的关键。任何一个傻瓜都能写出计算机可以理解的代码。唯有写出人类容易理解的代码,才是优秀的程序员。

第二章:重构原则

何谓重构

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

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

  3. 定义扩展
    重构的目的是使软件更容易被理解和修改。你可以在软件内部做很多修改,但必须对软件可观察的外部行为只造成很小变化,或甚至不造成变化。与之形成对比的是性能优化。和重构一样,性能优化通常不会改变组件的行为(除了执行速度),只会改变其内部结构。但是两者出发点不同:性能优化往往使低吗较难理解,但为了得到所需的性能不得不那么做。要强调的第二点:重构不会改变软件的可观察行为-重构之后软件功能一如既往。

  4. 两顶帽子
    使用重构技术开发软件时,你把自己的时间分配给两种截然不同的行为:添加新功能、重构。你可能会发现自己经常变换帽子。首先你会尝试添加新功能,然后会意识到:如果把程序结构改一下,功能的添加会容易得多。于是你换了一顶帽子,做一会重构工作。程序结构调整好后,又换上原来的帽子,继续添加新功能。新功能正常工作后,又发现自己的编码造成程序难以理解,于是又换上重构的帽子...

为何重构

1、重构改进软件设计 如果没有重构,程序的设计会逐渐腐败变质。当人民只为短期目的,或是在完全理解整体设计之前,就贸然修改代码,程序将逐渐失去自己的结构,程序员越来越难通过阅读源码理解原来的设计。重构就像是整理代码,你所做的就是让所有东西回到应处的位置上。代码结构的流失是积累性的。越难看出代码所代表的的设计意图,就越难保护其中设计,于是该设计就腐败的越快。经常性重构可以帮助代码维持自己该有的形态。
2、重构使软件更容易理解
3、重构帮助找到bug
4、重构提高编程速度
良好的设计是快速开发的根本-事实上,拥有良好设计才可能做到快速开发。如果没有良好设计,或许某一段时间内你的进展迅速,但恶劣的设计很快就让你的速度慢下来。你会把时间花在调试上面,无法添加新功能。修改时间越来越长,因为你必须花越来越多的时间去理解系统、寻找重复代码。随着你给最初程序打上一个又一个补丁,新特性需要更多代码才能实现,真是个恶性循环。

何时重构

1、 三次法则 事不过三,三则重构:第一次做某件事时只管去做;第二次做类似的事会产生反感,但无论如何还是可以去做;第三次再做类似的事,你就应该重构。
2、添加功能时重构 最常见的重构时机是我想给软件添加新特性的时候。此时,重构的直接原因是为了帮助我理解需要修改的代码。部分原因是为了让我下次再看这段代码时容易理解,最主要的原因是:如果在前进的过程中把代码结构理清,我就可以从中理解更多东西(说的不就是我吗)。 重构另一个原动力是:代码的设计无法帮助我轻松添加我所需要的特性。
3、修补错误时重构
4、复审代码时重构

怎么对经理说

在复审过程中使用重构使一个不错的方法。大量研究结果表明,技术复审时间少错误、提高开发速度的一条重要途径。然后你可以把重构当做“将复审意见引入代码内”的方法来使用。

重构的难题

1、数据库
重构经常出问题的一个领域就是数据库。绝大多数商用程序都与它们背后的数据库结构紧密耦合在一起,这也是数据库结构如此难以修改的原因之一,另一个原因是数据迁移。就算你非常小心地将系统分层,将数据库结构和对象模型间的依赖降至最低,但数据库结构的改变还是让你不得不迁移所有数据,这可能是件漫长而烦琐的工作。
2、修改接口
如果重构手法改变了已发布接口,你必须同时维护新旧两个接口,直到所有用户都有时间对这个变化做出反应。你通常都有办法把事情组织好,让旧接口继续工作,请尽量这么做:让旧接口调用新接口。当你要修改某个函数名称时,请留下旧函数,让它调用新函数。
3、难以通过重构手法完成的设计改动

何时不该重构

有时候你根本不应该重构,例如当你应该编写所有代码的时候。有时候既有代码实在太混乱,重构它还不如重新写一个来得简单。另外,如果项目已近最后期限,你也应该避免重构。

重构与设计

重构可以带来更简单的设计,同时又不损失灵活性,这也降低了设计过程的难度,减轻了设计压力。一旦对重构带来的简单性有更多感受,你甚至可以不必在预先思考前述所谓的灵活方案。

重构与性能

一个构造良好的程序可从两方面帮助这一优化形式。首先,它让你有比较充裕的时间进行性能调整,因为有构造良好的代码在手,你就能够更快速的添加功能,也就有更多时间用在性能问题上。其次,面对构造良好的程序,你在进行性能分析时便有较细的粒度,于是度量工具把你带入范围较小的程序段落中,而性能的调整也比较容易些。由于代码更加清晰,因此你能够更好地理解自己的选择,更清楚哪种调整起关键作用。 我发现重构可以帮助我写出更快的软件。短期看来,重构的确可能使软件变慢,但它使优化阶段的软件性能调整更容易,最终还是会得到好的效果。

第三章:代码的坏味道

Duplicated Code(重复代码)

最单纯的重复代码就是"同一个类的两个函数含有相同的表达式"。你需要做的就是采用提取方法提炼出重复的代码,然后让这两个地方都调用被提炼出来的那一段代码。
另一种常见情况就是“两个互为兄弟的子类中含有相同表达式”。要避免这种情况,只需对两个类都使用提取方法,然后再对被提炼的代码使用函数上移,将它推入超类中。如果代码之间只是类似,并非完全相同,那么就得运用提取方法将相似部分和差异部分分隔开,构成单独一个函数。然后你可能发现运用塑造模板方法的设计模式。如果有些函数以不同的算法做相同的事,你可以选择其中较清晰的一个,并使用替换算法将其他函数的算法替换掉。
如果两个毫不相关的类出现重复代码,你应该考虑对其中一个使用提取类,将重复代码提炼到一个独立类中,然后另一个类内使用这个新类。

Long Method(过长函数)

拥有短函数的对象会活的比较好、比较长。很久以前程序员就已经意识到:程序越长越难理解。
你应该更积极地分解函数。我们遵循这样一条原则:每当感觉需要以注释来说明点什么的时候,我们就把需要说明的东西写进一个独立函数中,并以其用途(并非实现手法)命名。我们可以对一组甚至短短一行代码做这件事,哪怕替换后的函数调用动作比函数自身还长,只要函数名称能够解释起用途,我们也该毫不犹豫这么做。关键不在于函数的长度,而在于函数“做什么”和“如何做”之间的语义距离。
如何确定该提炼哪一段代码呢?一个很好的技巧是:寻找注释,它们通常能指出代码用途和实现手法之间的语义距离。如果代码前方有一行注释,就是在提醒你:可以将这段代码替换成一个函数,而且可以再注释的基础上给这个函数命名。条件表达式和循环常常也是提炼的信号。你可以使用分解条件表达式的方式来处理条件表达式。至于循环,你应该将循环和其内的代码提炼到一个独立函数中。

Large Class(过大的类)

如果想利用单个类做太多事情,其内往往就会出现太多实例变量。一旦如此,重复代码就来了。你可以运用提取类的方法将几个变量一起提炼到新类中。提炼时应该选择类内彼此相关的变量,将他们放在一起。
和拥有太多实例变量一样,一个类如果拥有太多代码,往往也适合使用提取类和提取子类。这里有个技巧:先确定客户端如何使用他们,然后运用提取接口为每一种使用方式提炼出一个接口,这或许可以帮助你看清楚如何分解这个类。

Long Parameter List(过长参数列)

有了对象,你就不必把函数需要的所有东西都以参数传递给它了,只需传给它足够的让函数能从中活的自己需要的东西就行了。函数需要的东西多半可以在函数的宿主类中找到。面向对象程序中的函数,其参数列通常比在传统程序中短得多。这是好现象,因为太长的参数列难以理解,太多参数会造成前后不一致、不易使用,而且一旦你需要更多数据,就不得不修改它。如果将对象传递给函数,大多数修改都将没有必要,因为你很可能只需(在函数内)增加一两条请求,就能得到更多数据。

Divergent Change(发散式变化)

如果某个类经常因为不同的原因在不同的方向上发生变化,发散式变化就出现了。如果新加入一个数据库,必须修改三个函数,如果新出现一种金融工具,必须修改四个函数。那么此时也许将这个对象分成两个会更好,这么一来每个对象就可以只因一种变化而需要修改。针对某一外界变化的所有相应修改,都只应该发生在单一类中,而这个新类内的所有内容都应该反应此变化。为此,你应该找出某特定原因造成的所有变化,然后运用提取类的方法将他们提炼到另一个类中。

Shotgun Surgery(散弹式修改)

散弹式修改和发散式变化类似,但恰恰相反。如果每遇到某种变化,你都必须在许多不同的类内做出许多小修改,你所面临的就是散弹式修改。如果需要修改的代码散步四处,你不但很难找到它们,也很容易忘记某个重要的修改。这种情况下你应该使用移动方法和移动字段把所有需要修改的代码放进同一个类。如果眼下没有合适的类可以安置这些代码,就创造一个。通常可以运用内联类把一系列相关行为放进一个类,这可能会造成少量的发散式变化,但你可以处理。
发散式变化是指”一个类受多种变化的影响“,散弹式修改则是指”一种变化引发多个类相应修改“。

Feature Envy(依恋情结)

对象技术的全部要点在于:将数据和对数据的操作行为包装在一起。有一种经典现象是:函数对某个类的兴趣高于对自己所处类的兴趣。这种孺慕之情通常的焦点便是数据。多数情况下,我们看到某个函数为了计算某个值,从另一个对象那调用几乎半打的取值函数。怎么重构呢?把这个函数移至它该去的类中。有时候函数中只有一部分是这样,你可以使用提取函数把这一部分提取到独立函数中,再使用移动函数的方法移到合适的类中。

Data Clumps(数据泥团)

你尝尝在很多地方看到相同的三四项数据:两个类中相同的字段、许多函数签名中相同的参数。这些总是绑在一起出现的数据应该拥有属于他们自己的对象。首先找出这些数据以字段形式出现的地方,运用提取类将他们提炼到一个独立对象中。然后将注意力转移到函数签名上,运用引入参数对象或保留整个对象的方法为它”瘦身“。这么做的直接好处是可以将很多参数列缩短,简化函数调用。

Primitive Obsession(基本类型偏执)

对象的一个极大的价值在于:他们模糊(甚至打破)了横亘在基本数据和体积较大的类之间的界限。你可以轻松编写出一些与语言内置(基本)类型无异的小型类。所以能用对象的地方尽量用对象吧。

Switch Statements(switch惊悚现身)

面向对象程序的一个最明显的特征就是:少用switch(或case)语句。从本质上说switch语句的问题在于重复。你常会发现同样的switch语句散步与不同地点。如果要为他添加一个新的case子句,就必须找到所有switch语句并修改他们。面向对象中的多态可以带来优雅的解决办法。 switch语句常常根据类型码进行选择,你要的是”与该类型码相关的函数或类“,所以应该使用提取方法将switch语句提炼到一个独立函数中,再以移动方法将它搬移到需要多态性的那个类中。此时你需要决定是否使用”用子类代替类型码“或”用状态/策略模式代替类型码“。

Parellel Inheritance Hierarchies(平行继承体系)

平行继承体系是散弹式修改的特殊情况。在这种情况下,每当你为某个类增加一个子类,必须也为另一个类相应增加一个子类。如果你发现某个继承体系的类名称前缀和另一个继承体系的类名称前缀完全相同,便是闻到了这种坏味道。

Lazy Class(冗赘类)

你所创建的每一个类,都得有人去理解它,维护它,这些工作都是需要时间和金钱的。如果一个类的所得不值其身价,它就应该消失。这里所说的就是要消除代码里没有用处的类,或者用处很小的类需要进行重构。

Speculative Generality(夸夸其谈未来性)

这里是说那些我们现在暂时用不到但是却出现在我们代码里的函数,变量,函数参数以及其他的特殊情况。如果你的某个抽象类其实没有太大作用,请运用折叠继承体系。不必要的委托可以运用内联类将其除掉。如果函数的某些参数未被用上,可对它实施移除参数。如果函数名称带有多余的抽象意味,应该重新命名。

Temporary Field(令人迷惑的暂时字段)

如果类中有一个复杂算法,需要好几个变量,往往就可能导致暂时字段的出现。由于实现者不希望传递一长串参数,所以他把这些参数都放进字段中。但是这些字段只有再使用算法时才有效,其他情况下只会让人迷惑。这时候你可以利用提取类把这些变量和其他相关函数提炼到一个独立的类中,提炼后的新对象将是一个函数对象。

Message Chains(过度耦合的消息链)

如果用户向一个对象请求另一个对象,然后再向后者请求另一个对象,然后再请求另一个对象...这就是消息链。采取这种方式,意味着客户代码将于查找过程中的导航结构紧密耦合。一旦对象间的关系发生任何变化,客户端不得不做出相应修改。这时候应该用“隐藏委托关系”。理论上可以重构消息链上的任何一个对象,但这么做往往会把一系列对象都变成“中间人”。通常更好地选择是:先观察消息链最终得到的对象是用来干什么的,看看能否以“提取方法”把使用该对象的代码提炼到一个独立函数中,再使用“移动函数”把这个函数推入消息链。

Middle Man(中间人)

封装往往伴随委托。比如你问主管是否有时间参加一个会议,他就把这个消息“委托”给它的记事簿。但是人们可能过度使用委托。你可能看到某个类的接口有一半函数都委托给其他类,这样就是过度使用。如果这样“不干实事”的函数只有少数几个,可以运用“内联方法”把它们放进调用端。如果这些中间人还有其他行为,可以运用“用继承代替委托”把它变成子类,这样你既可以扩展原对象的行为,又不必负担那么多的委托动作。