《重构--改善既有代码的设计》阅读笔记

前段时间有空看了下重构,特地做了一下记录,方便以后反复阅读。

1 序章

下面我要告诉你:如何能够在不遍读全书的情况下得到最多知识。
  • 如果你想知道重构是什么,请阅读第1章,其中示例会让你清楚重构过程。
  • 如果你想知道为什么应该重构,请阅读前两章。它们告诉你「重构是什么」以及「为什么应该重构」。
  • 如果你想知道该在什么地方重构,请阅读第3章。它会告诉你一些代码特征,这些特征指出「这里需要重构」。
  • 如果你想真正(实际〉进行重构,请完整阅读前四章,然后选择性地阅读重构名录(refactoring catalog)。一开始只需概略浏览名录,看看其中有些什么,不必理解所有细节。一旦真正需要实施某个准则,再详细阅读它,让它来帮助你。名录是一种具备查询价值的章节,你也许并不想一次把它全部读完。此外你还应该读一读名录之后的「客串章节」,特别是第15章。

2 重构原则

2.1 为何重构
  • 重构改进软件设计
  • 重构使软件更容易理解
  • 重构帮助找到bug
  • 重构提高编程速度
2.2 何时重构
2.2.1 三次法则
  • Don Roberts给了我一条准则:第一次做某件事时只管去做;第二次做类似的事会产生反感,但无论如何还是可以去做;第三次再做类似的事,你就应该重构。
2.2.2 添加功能时重构
  • 当原代码的设计无法帮助我轻松添加我所需的特性时,重构是一个很好的选择。一旦完成重构,新特性的添加就会更快速、更流畅。
2.2.3 修补错误时重构
  • 如果收到一份错误报告,这就是需要重构的信号,因为显然代码还不够清晰——没有清晰到让你能一眼看出bug。
2.2.4 复审代码时重构
2.3 为什么重构有用
是什么让程序如此难以相与?眼下我能想起下述四个原因,它们是:
  • 难以阅读的程序,难以修改;
  • 逻辑重复的程序,难以修改;
  • 添加新行为时需要修改已有代码的程序,难以修改;
  • 带复杂条件逻辑的程序,难以修改。
因此,我们希望程序:
  • 容易阅读;
  • 所有逻辑都只在唯一地点指定;
  • 新的改动不会危及现有行为;
  • 尽可能简单表达条件逻辑.
重构是这样一个过程:它在一个目前可运行的程序上进行,在不改变程序行为的前提下使其具备。上述美好性质,使我们能够继续保持高速开发,从而增加程序的价值。
2.4 重构的难题
2.4.1 数据库
  • 重构经常出问题的一个领域就是数据库。绝大多数商用程序都与它们背后的数据库结构紧密耦合在一起,这也是数据库结构如此难以修改的原因之一。
2.4.2 修改接口
  • 简言之,如果重构手法改变了已发布接口,你必须同时维护新旧两个接口,直到所有用户都有时间对这个变化做出反应。幸运的是,这不太困难。你通常都有办法把事情组织好,让旧接口继续工作。请尽量这么做:让旧接口调用新接口。当你要修改某个函数名称时,请留下旧函数,让它调用新函数。千万不要复制函数实现,那会让你陷入重复代码的泥淖中难以自拔。
  • 发布接口很有用,但也有代价。所以除非真有必要,不要发布接口。这可能意味需要改变你的代码所有权观念,让每个人都可以修改别人的代码,以适应接口的改动。以结对编程的方式完成这一切通常是个好主意。
  • tips:不要过早发布接口。请修改你的代码所有权政策,使重构更顺畅。
2.4.3 难以通过重构手法完成的设计改动
  • 遇到难以重构的情况下我的办法就是:先想象重构的情况。考虑候选设计方案时,我会问自己:将某个设计重构为另一个设计的难度有多大?如果看上去很简单,我就不必太担心选择是否得当,于是我就会选最简单的设计,哪怕它不能覆盖所有潜在需求也没关系。但如果预先看不到简单的重构办法,我就会在设计上投入更多力气。不过我发现,后一种情况很少出现。
2.4.4 何时不该重构
  • 有时候你根本不应该重构,例如当你应该重新编写所有代码的时候。有时候既有代码实在太混乱,重构它还不如重新写一个来得简单。作出这种决定很困难,我承认我也没有什么好准则可以判断何时应该放弃重构。
  • 重写(而非重构)的一个清楚讯号就是:现有代码根本不能正常运作。你可能只是试着做点测试,然后就发现代码中满是错误,根本无法稳定运作。记住,重构之前,代码必须起码能够在大部分情况下正常运作。
  • 一个折中办法就是:将“大块头软件”重构为封装良好的小型组件。然后你就可以逐一对组件做出“重构或重建”的决定。这是一个颇有希望的办法,但我还没有足够数据,所以也无法写出好的指导原则。对于一个重要的遗留系统,这肯定会是一个很好的方向。
  • 另外,如果项目已近最后期限,你也应该避免重构。在此时机,从重构过程赢得的生产力只有在最后期限过后才能体现出来,而那个时候已经为时晚矣。如果最后你没有足够时间,通常就表示你其实早该进行重构。
2.5 代码的坏味道
2.5.1 Duplicated Code (重复代码)
  • 坏味道行列中首当其冲的就是DuplicatedCode。如果你在一个以上的地点看到相同的程序结构,那么可以肯定:设法将它们合而为一,程序会变得更好。
2.5.2 Long Method (过长函数)
  • 拥有短函数的对象会活得比较好、比较长。
  • 最终的效果是:你应该更积极地分解函数。我们遵循这样一条 原则:每当感觉需要以注释来说明点什么的时候,我们就把需要说明的东西写进一个独立函数中,并以其用途(而非实现手法)命名。
  • 如何确定该提炼哪段代码呢?一个很好的技巧是:寻找注释。它们通常能指出代码用途和实现手法之间的语义距离。如果代码前方有一行注释,就是在提醒你:可以将这段代码替换成一个函数,而且可以在注释的基础上给这个函数命名。就算只有一行代码,如果它需要以注释来说明,那也值得将它提炼到独立函数去。
  • 条件表达式和循环常常也是提炼的信号。你可以使用Decompose Conditional (238)处理条件表达式。至于循环,你应该将循环和其内的代码提炼到一个独立函数中。
2.5.3 Large Class (过大的类)
  • 如果想利用单个类做太多事情,其内往往就会出现太多实例变量。一旦如此,Duplicated Code也就接踵而至了。
2.5.4 Long Parameter List (过长参数列)
  • 太长的参数列难以理解,太多参数会造成前后不一致、不易使用,而且一旦你需要更多数据,就不得不修改它。如果将对象传递给函数,大多数修改都将没有必要,因为你很可能只需(在函数内)增加一两条请求,就能得到更多数据。
2.5.5 Divergent Change (发散式变化)
2.5.6 Shotgun Surgery (霰弹式修改)
  • Divergent Change是指“一个类受多种变化的影响”, Shotgun Surgery则是指“一种变化引发多个类相应修改”。这两种情况下你都会希望整理代码,使“外界变化”与“需要修改的类”趋于一一对应。
2.5.7 Feature Envy (依恋情结)
  • 将总是一起变化的东西放在一块儿。数据和引用这些数据的行为总是一起变化的,但也有例外。如果例外出现,我们就搬移那些行为,保持变化只在一地发生。Strategy和Visitor使你得以轻松修改函数行为,因为它们将少量需被覆写的行为隔离开来一当然也付出了“多一层间接性”的代价。
2.5.8 Data Clumps (数据泥团)
  • 一个好的评判办法是:删掉众多数据中的一项。这么做,其他数据有没有因而失去意义?如果它们不再有意义,这就是个明确信号:你应该为它们产生一个新对象。
2.5.9 Comments (过多的注释)
  • 你看到一段代码有着长长的注释,然后发现,这些注释之所以存在乃是因为代码很糟糕。这种情况的发生次数之多,实在令人吃惊。
  • 当你感觉需要撰写注释时,请先尝试重构,试着让所有注释都变得多余。
  • 如果你不知道该做什么,这才是注释的良好运用时机。除了用来记述将来的打算之外,注释还可以用来标记你并无十足把握的区域。你可以在注释里写下自己“为什么做某某事”。这类信息可以帮助将来的修改者,尤其是那些健忘的家伙。
2.6 重构手法
2.6.1 Extract Method (提炼函数)
  1. 创造一个新函数,根据这个函数的意图来对它命名(以它“做什么”来命名,而不是以它“怎样做”命名)。即使你想要提炼的代码非常简单,例如只是一条消息或一个函数调用,只要新函数的名称能够以更好方式昭示代码意图,你也应该提炼它。但如果你想不出一个更有意义的名称,就别动。
  2. 将提炼出的代码从源函数复制到新建的目标函数中。
  3. 仔细检查提炼出的代码,看看其中是否引用了“作用域限于源函数”的变量(包括局部变量和源函数参数)。
  4. 检查是否有“仅用于被提炼代码段”的临时变量。如果有,在目标函数中将它们声明为临时变量。
  5. 检查被提炼代码段,看看是否有任何局部变量的值被它改变。如果一个临时变量值被修改了,看看是否可以将被提炼代码段处理为一个查询,并将结果赋值给相关变量。如果很难这样做,或如果被修改的变量不止一个,你就不能仅仅将这段代码原封不动地提炼出来。你可能需要先使用Split Temporary Variable (128),然后再尝试炼。也可以使用Replace Temp with Query (120)将临时变量消灭掉(请看“范例”中的讨论)。
  6. 将被提炼代码段中需要读取的局部变量,当作参数传给目标函数。
  7. 处理完所有局部变量之后,进行编译。
  8. 在源函数中,将被提炼代码段替换为对目标函数的调用。如果你将任何临时变量移到目标函数中,请检查它们原本的声明式是否在被提炼代码段的外围。如果是,现在你可以删除这些声明式了。
  9. 编译,测试。
2.6.2 Inline Method (内联函数)
  1. 检查函数,确定它不具多态性。如果子类继承了这个函数,就不要将此函* 数内联,因为子类无法覆写一个,根本不存在的函数。
  2. 找出这个函数的所有被调用点。
  3. 将这个函数的所有被调用点都替换为函数本体。
  4. 编译,测试。
  5. 删除该函数的定义。
  6. 被我这样一写, Inline Method (117)似乎很简单。但情况往往并非如此。对于递归调用、多返回点、内联至另一个对象中而该对象并无提供访问函数....每一种情况我都可以写上好几页。我之所以不写这些特殊情况,原因很简单:如果你遇到了这样的复杂情况,那么就不应该使用这个重构手法。
2.6.3 Inline Temp (内联临时变量)
  1. 检查给临时变量赋值的语句,确保等号右边的表达式没有副作用。
  2. 如果这个临时变量并未被声明为final,那就将它声明为final,然后编译。这可以检查该临时变量是否真的只被赋值一次。
  3. 找到该临时变量的所有引用点,将它们替换为“为临时变量赋值”的表达式。
  4. 每次修改后,编译并测试。
  5. 修改完所有引用点之后,删除该临时变量的声明和赋值语句。
  6. 编译,测试。
2.6.4 Replace Temp with Query (以查询取代临时变量)
2.6.5 Introduce Explaining Variable (引入解释性变量)

下载地址自行百度,网上一大堆电子书。看着例子学习一下还是能有所收获的。

转载于:https://juejin.im/post/5c9217d6f265da611f1d85f2

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值