0%

《重构》读书笔记

第一章 重构概述

重构的定义

重构的定义:在不改变软件可观察行为的前提下,调整其结构,提高其可理解性,降低其修改成本。

重构的关键在于运用大量微小保持软件行为的步骤,一步一步完成大规模的修改。即使重构没有完成,也可以随时停下来,保证代码的运作。

对于“可观察行为”的理解:从用户所关心的角度而言,重构前后的软件表现应当一致。如果重构前有bug,重构后应该保留bug。但是重构所带来的性能提升、接口改变、调用栈改变甚至潜在bug的优化都不放在“可观察”的范围内。

重构的意义

  • 改进软件的设计。消除重复代码。
  • 使软件更容易理解。代码的读者不只是机器,还有人。一次将所理解的内容表达清楚,就避免了下一个人浪费时间重新理解。
  • 帮助找到bug。重构时需要试图理解代码,并将新的理解反映在新代码当中,这个过程使bug暴露。
  • (最终意义)提高编程速度。不及时做重构的团队在添加新功能时,软件的质量会越来越差,添加代码会非常困难。而及时重构的团队能够利用已有功能快速构建新功能,好的代码更容易理解、更容易修改

行业陈规认为,良好的设计必须在开始编程之前完成,编程时设计只会逐渐腐败。新的观念认为,先做好设计,然后在编码和重构中不断改善它——“设计耐久性假说”:投入精力改善内部设计,可以延长软件的耐久性

两顶帽子:“添加新功能”的帽子、“重构”的帽子。在开发程序的过程中会换帽子:添加功能时,我发现代码结构需要稍微调整调整,于是换上重构的帽子,重构完成后,继续添加新功能。

重构的示例

现在需要添加新特性,但是发现代码结构性差,因而需要重构。重构后,代码会便于添加特性。下面是一次重构的示例。

  1. 首先,需要一个可靠的测试集:可以自我检测(自己对比运行结果的不同)
  2. 分解一个大函数
    1. 提炼函数(copy出来)
      1. 考虑:那些变量离开了作用域?两种处理:
        1. 会被新函数使用但不会被修改,则可作为传入参数;会被修改,则可作为返回值。
        2. 可在新函数中初始化
      2. 编译、测试、提交。之后每一个小操作之后都要小步提交,论小步修改的重要性。
    2. 提升代码的表达能力:针对新函数
      1. 优化命名:起最好的名字,起最好的函数名配合返回值result使用
      2. 将函数的返回值命名为result。
      3. 参数命名:a/an+类型名。
    3. 移除不必要的参数
      1. 原函数:以查询取代临时变量。(函数代替变量)
        1. 内联消除变量:构造一个新的用于查询的函数,以函数调用返回值作为参数,删除临时变量。
        2. 以查询取代临时变量,需要先将功能相关的代码都集中到一起,拆分循环。
      2. 新函数精简参数:调用新函数替代传入参数;删掉传入参数,删掉调用方的传入参数;
    4. 重复上面三步。

第二章 重构的时机和挑战

重构的时机

三次法则、事不过三:第一次做某件事时只管去做;第二次做类似的事会产生方案,但无论如何还是可以去做;第三次在做类似的事,你就应该重构。——Don Roberts

预备性重构:使添加新代码更加容易

重构的最佳时机是在添加新功能前:对代码做一点调整,使得添加新功能更加容易。

具体体现:利用和改造已有的代码,避免添加增添代码。而且,可能在合并相似功能的过程中,简化某些bug的复杂度,使得bug更容易找到并解决。

几个函数有类似的功能,可以使用函数参数化的方式合并一些类似功能。

帮助理解的重构:使代码更加易懂

重构前要理解代码在做什么。通过重构,要把脑袋里对代码的理解转化为代码本身。

糟糕的代码理解起来要花费更多时间;好的代码不需要那么多的精力去理解,并且不需要额外花费时间去记忆,因为它本身承载了足够多的信息。通过一次重构将代码转化为容易理解的代码,会让后续其他开发人员(或自己)添加代码更加方便,节省更多的时间。

研读代码时,重构会引领我们获得更高层面的理解,如果只是阅读代码很难有此领悟。——Ralph Johnson

捡垃圾式重构

已经理解代码在做什么,并知道代码中存在垃圾,重构掉这些垃圾会更容易增加代码。也可以在没事的时候捡捡这些垃圾。

重构的妙处在于什么时候捡垃圾、捡多少都不会影响软件功能。

有计划的重构和见机行事的重构

与前面三种不同,这次是主动重构。不只是垃圾代码需要重构,漂亮代码也需要重构,只是漂亮的代码重构起来更加容易。

添加新功能最快的方法是先修改现有代码,使新功能容易被加入。所以,有计划的重构是有好处的,但是大部分的重构还是见机行事的。

重构可以单独放在一个分支上做,但是重构一般和添加新功能密不可分,因为这样才能感受到重构的价值。如果独立重构的分支也有好处,那就独立。

每次要修改时,首先令修改很容易(警告:这件事有时会很难),然后在进行这次容易的修改。——Kent Beck

长期重构

有些大型重构需要花很长的时间。

例如,如果想替换掉一个正在使用的库,可以先引入一层新的抽象,使其兼容新旧两个库的接口。一旦调用方已经完全改为使用这层抽象,替换下面的库就会容易得多。——Branch By Abstraction

复审代码时重构

把重构作为code review的一种方式。这样可以对代码进行更高层次的复审,也能够提出更有价值的检视意见。

与原作者一起浏览代码并重构是最好的,这种工作方式叫做结对编程

思考:把重构作为一种检视方法,可以归类为“以输出的方式输入”的方法(费曼学习法)。我们读书、思考并总结出自己的知识点,写成了博客分享给别人,是同理的。

重构的其他情况

部分经理、客户不具备足够的技术意识,不认同重构对于代码的长期意义。这种情况下,干脆不要告诉经理自己要重构!作为软件开发人员,只是选择了一种快速开发的方法而已,采用具体什么方式对于上层管理者是不可见的。

不合适重构的情况

  1. 一块垃圾代码不需要修改时,没必要去重构。
  2. 重构的效率还没有重写的效率高时,没必要去重构。一切工作的导向是开发效率。

重构的挑战

重构延缓新功能的开发

重构的唯一目的就是开发更快,用更少的工作量创造更大的价值。所以要不要重构是需要权衡和决策的。

事实是,行业中大部分情况是重构不足。合理判断何时应该重构、何时应该暂时不重构,这样的判断力需要多年经验积累,毕竟重构还是由经济利益驱动的,如果认为重构有价值,应当懂得说服其他人——本次重构对于经济成本的节约等等。

代码所有权

(待续)

第三章 重构对架构和性能的影响

重构改变软件架构方式

重构打破了软件一旦开发完毕就会日渐腐败的行业陈规。我们在重构的定义、意义和一次示例当中就学习到“设计耐久性假说”这一观点,即重构可以修改调整老代码的结构从而延长软件的生命周期。

通过重构,开发人员可以应对不断变化的需求,因而不再拘泥于“编码前必须完成架构”的观点。

引入灵活性机制

为了应对不断变化的需求,要在软件里植入灵活性机制。例如,在编写函数时提前为函数参数做打算,预留几个参数迎接未来可能的需求,来提升函数的通用性。

可是,我们预测的需求往往和实际不符。这时候应该发挥重构的优势:按照当前需求将软件的设计质量做得很高,随着用户需求的不断累积,及时对架构进行重构。

考虑是否值得引入灵活性机制:能够应对需求且不会增加复杂度,可以引入灵活性机制;能够预测未来此处会变的难以重构时,现在就值得引入灵活性机制。

YAGNI(You aren’t going to need it)

上述的开发和决策过程被成为YAGNI方法,是一种将架构、设计与开发过程融合的工作方式,需要有重构作为基础。

YAGNI可以理解为:编码前的架构仍要足够完善,但不必尽善尽美,等到对问题的理解更为充分时,再着手解决。即一种演进式架构。

重构对性能的影响:可读性vs性能

绝不能为了提高设计质量而忽视性能,也绝对不能寄托于高速的硬件。有时,我们为了照顾软件的可读性,往往会牺牲软件的性能,这一点可以理解为:虽然重构使软件运行更慢,但是它使软件的性能优化更容易。

所以,可读性和性能的平衡点在于,先写出方便调优的软件,再调优它以获得足够的性能。短期看来,重构可能使软件变慢,但是它使优化阶段的软件性能调优更加容易,长期看来,重构对程序的性能有好处。

对于性能,有一件有趣的事情:一大半时间都耗费在一小半代码上。所以,一视同仁的优化所有代码,则效率非常低,90%的工作都是白费。针对这一现象,我们在编写代码时,只关注创造良好的程序,但是到开发后期——进入性能优化阶段时,可以善用性能度量工具来筛出耗费大量时间和空间的代码,集中关注这些性能热点进行性能优化。

第四章 代码坏味道

闻到坏味道,就应当去重构

何时重构及何时停止和直到重构机制如何运转一样重要。当闻到代码的坏味道,就应当去重构,而这个近乎直觉的习惯需要开发者长期培养。

坏味道与对应的重构手法

糟糕的命名

命名是编程中最难的两件事之一。

编程时要深思熟虑给函数、模块、变量和类命名。另外,如果想不出一个好名字,说明背后可能有潜藏的设计问题。

重构手法:改名

  1. 改变函数声明
  2. 变量改名
  3. 字段改名

重复代码

要修改重复代码时,要将全部的重复代码都找出来,对比其间差异,合二为一。

重构手法

  1. 提炼函数:将代码中多处重复的部分提炼出一个新的函数。
  2. 移动语句:重组代码顺序 ,把相似的部分放在一起以便提炼。结合提炼函数的方法。
  3. 函数上移:如果重复代码出现在同父的多子类之中,使用此方法来避免在子类之间相互调用。

过长函数

过长的函数难以理解、复用性差。短小的函数具有更好的阐释力、更易于分享、提供更多的使用选择。好的程序善用短小的函数。这一点也要仰仗于函数良好的命名,使开发者通过函数名直接了解函数作用。

重构思路

积极地分解函数:每当感觉要以注释来说明时,就考虑封装一个独立的函数,并以用途命名。关键不在于函数的长度,而在于函数“做什么”和“如何做”之间的语义距离。

重构手法

  1. 提炼函数:拆分大函数。
  2. 以查询取代临时变量:当大函数的参数和局部变量过多,对提炼函数有障碍时,考虑以查询获取数据的方法取代临时变量。
  3. 引入参数对象:将多个数据封装成一个数据结构。这种方法会催生代码的深层次改变。
  4. 保持对象完整:传递一个结构的多个值给一个函数,不如直接传递整个结构。常发生在引入参数对象的手法之后。
  5. 以命令取代函数:采用上述手法后仍然有大量参数和临时变量,可采用此方法。
  6. 分解条件表达式:例如,对switch的每个分支处理提炼函数。
  7. 以多态取代条件表达式:多个switch语句基于同一个条件进行分支选择时,可以将每个分支的逻辑创建一个类,用多态来承载各个类型特有的行为,从而去除重复的分支逻辑。
  8. 拆分循环:每个循环只做一件事情。先保证结构优化,再考虑性能。
    1. 将循环本身和循环内的代码提炼到一个独立的函数中;
    2. 将循环中多个动作分发到不同的循环中,然后运用提炼函数的手法。

过长参数列表

重构思路

总之不要指望靠全局变量能解决这个问题!

重构手法

  1. 以查询取代参数:向某个参数发起查询来获取另一个参数。
  2. 保持对象完整:传递一个结构的多个值给一个函数,不如直接传递整个结构。
  3. 引入参数对象:将多个数据封装成一个数据结构。
  4. 移除标记参数:
    1. 函数指望一种标记参数来指示自己执行哪一条逻辑,不如将每条逻辑拆成单独的函数。
    2. 注意区分标记参数,它影响了函数内部的控制流,并非函数内部实现。
    3. bool类型的标记参数无法表明确切含义。
  5. 函数组合成类:多个函数有同样的几个参数,可以引入类,将一些方法和数据绑定在一起。
    1. 类能明确地给函数提供一个共用的环境,在类的内部,函数参数会减少很多。
    2. 类可以更方便地传递给其他系统。

全局数据

重构思路

全局数据:全局变量、类变量、单例等。

全局数据会造成诡异的bug,思路就是要控制它的作用域。

重构手法

  1. 封装变量:将对全局数据的修改封装起来,开始控制它,将这个函数搬移到一个类或模块当中,以限制其作用域。