Skip to content

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


重构的定义

重构,名词解释为:对软件内部结构的一种调整,木都市在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本;动词解释为:使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。

重构的关键在于运用大量微小且保持软件行为的步骤,一步步达成大规模的修改。因此,如果有人说他们的代码在重构过程中有一两天的时间不可用,基本可以确定,他们在做的事不是重构。

为何重构(重构的意义)

1. 重构改进软件的设计

当人们只为短期目的而修改代码时,经常没有完全理解架构的整体设计,于是代码逐渐失去自己的结构,导致程序员越来越难通过阅读源码来理解原有设计,就越难保护其原有设计,于是设计就腐败的越快。代码设计越腐败,就越难做正确的修改,往往出现修改一点,系统却不如期工作了。经常性的重构有利于代码维护自己该有的形态。

2. 重构使软件更容易理解

“傻瓜都能写出计算机可以理解的代码,唯有能写出人类容易理解的代码,才是优秀的程序员”。开发过程中容易更关注功能的实现,而忽略了代码可读性,导致后来者要花费大量时间来理解你的代码意图。通过重构更清晰的说出自己想要做的(意图),帮助我们让代码更易读。

3. 重构帮忙找到bug

通过重构可以深入理解代码的所作所为,并立即把新的理解反映在代码中,搞清楚程序结构的同时,也验证了自己所做的一些假设,也就轻松找到了bug。重构帮助我们更有效的写出健壮的代码。

4. 重构提高编程速度

通过重构维持了原有架构设计,并且使代码更易读、更健壮,此时添加一个新功能或修改一个已有功能的速度也就越快。可能重构过程中会花费一些时间,但是ROI(投入产出比)仍然是显著的。

何时重构

事不过三,三则重构

添加新功能或者修复Bug或者code review时,可以不合理的地方进行重构 何时不应该重构呢? 当一块凌乱的代码,但并不需要修改它时,就不需要重构,或者丑陋的代码隐藏在API之下,就不需要重构。还有一种情况是,重写比重构还容易的话,那就没有必要重构了。

重构的挑战

  1. 延缓新功能开发。 这种情况需要权衡取舍,如果添加的功能非常小,那可以先添加功能,然后再做重构。之所以重构并不仅因为“整洁的代码”、“良好的工程实践”等道德理由,而是因为它能让我们更快的添加新功能,更快的修复Bug,更快的进行工程编译。
  2. 代码所有权。 当修改的代码有上层业务依赖时,需要做好代码兼容,可以标记旧接口为“不推荐使用”(deprecated),或者在旧接口中调用新街口,实现调用无感知过度
  3. 分支管理。 代码在各自分支修改,当合并是就会出现冲突问题,此时可以考虑CI来降低冲突问题出现概率。
  4. 遗留代码。 遗留代码简直就是噩梦般的存在,遗留代码往往很复杂,测试又不足,关键是别人写的。面对这种代码时,不建议一鼓作气进行重构,而是每次触碰一块代码时,尝试进行重构,并找到程序的接缝处,插入测试进行充分自测。
  5. 数据库。 数据库重构时最容易出现问题的地方。建议小步修改,并且每次修改都应完整,上线一段时间发现没有Bug后,再进行下一小步的修改。

代码中的怪味道

1. 神秘命名

整洁代码最重要的一环就是好的名字,好的命名可以清晰的表明自己的功能和用法。

2. 重复代码

对于重复的代码需要根据场景进行分析,是否可以进行合并。并不是所有的重复代码都需要合并,例如两个功能目前都是一样的代码,但在后面可能会对单个功能进行修改或拓展,此时是不需要合并的。

3. 过长函数

函数越长,意味着里面包含了很多复杂的逻辑,也就越难理解,越难理解就会导致修改时出现Bug,所以可以对过长函数进行合理拆分,将部分能独立的功能,单独抽取为一个单独地函数。

4. 过长参数列表

调用函数需要参数过多时,就会让调用方迷惑,建议当需要多个参数时,可以将参数封装成一个对象,这样方便调用方知道参数的含义以及哪些是必须的,哪些是可选的。

5. 全局数据

全局数据的问题在于,代码库中的任何角落都可以修改它,而且没有任何机制可以探测到底那段代码做出了修改,可能会造成各种诡异的Bug。所以当有全局数据的时候可以考虑用一个函数进行包装,至少这样可以看到哪里修改了它,并可以控制它的访问,最好将这个函数搬移到一个类或模块中,尽量控制其作用域。

6. 可变数据

可变语句与全局数据有异曲同工之妙,都是可以随意修改它,导致容易出现各种诡异的Bug。可以通过函数封装,将更新操作控制在很少几个函数来进行;或者对变量进行不同用途的拆分,从而避免危险的更新操作;或者对变量进行提炼分离,将没有副作用的代码与执行更新操作的代码分开,确保调用者不会调用到有副作用的代码。

7. 发散式变化

发散式变化是指如果某个模块经常因为不同的原因在不同的方向上发生变化,就会出现发散式变化。例如,新加入一个数据库,必须修改3个函数;如果新加一种金融工具,必须修改4个函数,这就是发散式变化的前兆。数据库交互与金融逻辑处理是两个不同的上下文,将他们分别搬移到各自独立的模块中,能让程序变的更好:每当要对某个上下文做修改时,我只需要理解这个上下文,而不需要操心另一个。

8. 霰弹式修改

霰弹式修改与发散式变化类似,但恰恰相反。当遇到某种变化时,你必须在许多个不同的勒种做出许多小修改,这就是霰弹式修改。这种情况可以将相同的逻辑放入同一个模块中等操作缩小修改范围。

9. 依恋情节

单个函数与另一个模块中的函数或数据交流格外频繁,远胜于在自己所处模块内部的交流,这就是依恋情节的典型情况。简单的场景可以直接将该函数移到它的梦想家园,略复杂的可以先提炼函数,然后再搬移函数。

10. 数据泥团

数据泥团是指一个数据类包含了很多数据项,而有些字段、函数与其他数据类型重复。出现此场景时,可以考虑将相同的字段或函数单独提前为一个独立对象,然后通过组合的方式进行调用。

11. 基本类型偏执

基本类型偏执是指:对一个字段指定了基本类型,但后续的判断、转换等都是直接使用该字段,导致维护成本较高问题。对于这种字段,可以考虑用一个类来封装,这样后续的判断、转换等都可以在该类中进行拓展。

12. 重复的switch

重复的switch是指:在不同的地方反复使用同样的switch逻辑(可能是以switch/case语句形式,也可能是以连续的if/else语句的形式),这样当你需要增加一个选择分支时,必须找到所有的switch,并逐一更新才行。对于这种场景可以使用多态来替换条件表达式。

13. 循环语句

对于循环语句并没有什么问题,而是可以使用更优的方法——以管道取代循环(如fliter、map等),你会发现,使用管道操作可以帮助你更快的看清被处理元素以及处理它们的动作。

14. 冗余的元素

一个函数,它的名字跟实现代码看起来一模一样,或者一个类根本就一个简单的函数,可能在编写初期,程序猿期望它将来有一天会变大、变复杂,但哪一天从未到来,对于这种场景就是冗余的元素,可以理解为前期的过度设计导致了这种场景。对于这种元素可以进行拆分或者内联。

15. 夸夸其谈的通用性

当有人说“噢,我想我们总有一天需要做这事”,并因而企图以各式各样的钩子和特殊情况来处理一些非必要的事情,这就是夸夸其他的通用性。这么做的结果往往造成系统更难理解和维护。如果所有装置都会被用到,就值得那么做;如果用不到,就不值得。

16. 临时字段

某个类内部某个字段仅为某种特定情况而设。这样的代码让人不易理解,因为你通常认为对象在所有时候都需要它的所有字段。在字段未被使用的情况下猜测当初设置它的目的,会让你发疯。对于这种场景,可以将字段单独提炼一个类,然后将所有与这个字段相关的代码都搬移到该类中。

17. 过长的消息链

过长的消息链是指数据类中嵌套多层数据类型,这样就会出现一个对象请求另一个对象,然后再向后者请求另个一对象,然后再请求另个一对象,最终才能知道想要的对象。例如,个人信息数据类中包含部门,部门数据类型里又包含的二级部门,二级部门里面又包含三级部门,此时要想拿到这个人属于哪个三级部门时,需要一层一层的往下找才可以。对于这种情况可以隐藏委托关系,在个人信息也中将该数据用函数返回,避免在获取最底层中也可以使用中间层的情况出现,降低不可控性。

18. 中间人

当出现某个类的接口有一半的函数都委托给其他类,这就是过度运用委托(即中间人)。这种情况可以直接移除中间人,直接和真正负责的对象打交道,或者如果这种函数较少时,也可以直接通过内联函数把他们放进调用端。

19. 内幕交易

如果两个模块总是私自交流,就应该将交流的函数或者字段搬移,减少它们的私下交流。如果两个模块有共同的兴趣,可以尝试再新建一个模块,把这些共用的数据放在一个管理良好的地方,或通过隐藏委托关系,把另一个模块变成两者的中介。集成常会造成密谋,因为子类对超类的了解总是超过后者的主管愿望。如果你觉得该让这个孩子独立生活了,就可以运用以委托取代子类或以委托取代超类,让它们离开集成体系。

20. 过大的类

如果一个类做了太多事情,其内往往就会出现太多字段,重复的代码就会接踵而至。对于这种情况可以提炼出子类。

21. 异曲同工的类

使用类的好处之一就在于可以替换,但只有当两个类的接口一致时,才能做这种替换。可以用函数改名方法将函数签名变的一致,并反复运用搬移函数方法将某些行为移入类中,直到两者的协议一致为止。

22. 纯数据类型

所谓纯数据类是指:他们拥有一些字段,以及用于访问(读写)这些字段的函数,除此之外一无长物。可以将操作(读写)封装到数据类中,对于不该被其他类修改的字段,可以删除赋值函数(写操作)。

23. 被拒绝的遗赠

对于子类不想继承超类的函数和数据或仅继承部分时,就意味着集成体系设计错误,应该为这个子类创建一个兄弟类,将所有用不到的函数下推给那个兄弟。如果子类复用了超类的行为,但又不愿意支持超类的接口时,就不必虚情假意的糊弄继承体系,应该运用"以委托取代子类"或"以委托取代超类"彻底划清界限。

24. 注释

注释这里的坏味道是指:一段代码有着长长的注释,然后发现这些注释之所以存在是因为代码很糟糕。这里不应该将注释当做“除臭剂”来使用。

Released under the MIT License.