阅读 155

读书·技术 |《重构》· 重构的原则

最近在梳理既有业务和新增功能特性的过程中,发现代码的结构和味道已经是系统演进的突出瓶颈了。

中间层的缺失,让重构牵一发而动全身。合理地引入中间层已经是系统重构的必要举措。

David Wheeler 对于中间层的经典描述:

All problems in computer science can be solved by another level of indirection, except of course for the problem of too many indirections.


在持续进行系统开发的过程中,要关注明天价值。

程序的价值分为两个方面,即“今天能为你做什么”和“今后能为你做什么”。

我们常常只关注前者,这就造成了程序“我今天能为你工作,明天我将完全无法胜任工作”的困境。


需要重构的代码往往都是难以修改的:

  1. 难以阅读。
  2. 逻辑重复。
  3. 添加新的行为需要修改已有代码。
  4. 带有复杂条件逻辑。


重构以后的代码需要达到的目标是:

  1. 容易阅读。
  2. 相同的逻辑只出现一次。
  3. 新的改动不会危及现有行为。
  4. 尽可能简单地表达条件逻辑。


何为重构

重构,可以说既是一个名词又是一个动词。

从名词的意义上定义,重构就是对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。

从动词的意义上定义,重构就是使用一系列重构手法,在不改变软件可观察行为的前提下,调整起结构。

从上面的定义可以看到,重构的目的是让软件更易于理解和修改。但从外部来看,重构造成的修改对可观察的外部行为只造成很小的改变,甚至不造成改变。重构和性能优化一样,通常不会改变组件的行为,只是改变其内部结构,只是性能优化还会改变执行速度;但两者的出发点不同,性能优化有时候会让代码更加难以理解和修改,这也是为了提高性能不得不付出的代价。


为何重构

重构改进软件设计

代码结构的流失是累计性的,越是难以看出代码所代表的设计意图,就越是难保护其设计,于是设计就腐败的越快。如果没有重构,代码就会腐败变质,而经常性的重构可以帮助代码维持起该有的形态。

重构使软件更容易理解

软件是写给计算机读的,也是写给开发者读的,你需要让计算机知道你所要达到的意图,同样也需要让其他开发者知道软件的意图。事实上,有时候我们常常不需要记忆自己写过的代码,因为当我们需要了解软件的时候,只要打开形成它的代码,就应该对“想要干什么”和“能干什么”了如指掌了。

软件应该是“自描述”的。

随着代码的趋于简洁,你可以看到之前没有发现的一些设计意图,重构不仅仅是调整代码的细枝末节,还能帮助你对软件获得更高层次的理解。

重构帮助找到bug

对于bug而言,当程序比较简单的时候,通过阅读代码就可以发现里面是否有bug。但是随着程序的复杂,光看代码已经很难发现bug了。调试也许可以帮助你更好地发现bug,但是并不是所有程序都有良好的调试条件。面对复杂的代码,重构的过程可以让你深入理解代码的意图和细节,从而让你发现其中的bug。

重构提高开发效率

对于重构提高开发质量大家已经达成共识了,但是很多人忽略了重构也可以提高开发速度。

对于糟糕的软件,我们需要花大量的时间阅读代码去理解它的设计意图,同时需要耗费大量的精力寻找重复代码,给程序打上一个又一个的补丁。由于需要大量的时间调试代码,因此你很少有时间编写新的代码,开发效率也就大打折扣。

良好的设计是维持开发速度的根本。重构可以防止程序腐败变质,还可以提高软件的质量,是提高开发效率的根本。


何时重构

我们是不是应该抽出专门的几个星期时间来重构呢?事实上,重构不应该成为一个单独的任务,重构应该随时随地地进行。我们不要为了重构而重构,我们应该是为了做什么具体的事情而顺便进行重构。重构可以让你把应该做的事情做的更好。

三次法则

如果一件事需要做一两次,可以不着急重构;但是如果需要重复三次甚至以上的话,就该考虑着手去重构了。

事不过三,三次重构。

添加功能时重构

重构的直接原因是帮助我们理解代码的意图,当我们发现程序难以理解的时候,往往是重构代码的绝佳时机。

当我们发现添加新的功能不能良好的体现我们的设计意图的时候,也是重构的时机,重构可以让我们新增功能变得更加简单,也更能体现出设计意图。

修复错误的时候重构

如果你的程序出现了意想不到的bug,这正是你的代码需要重构的信号。因为你无法一眼看出程序中的bug。

修复错误的时候重构,正是为了帮助我们理解代码,从而更清晰地看到bug的所在。

评审代码时重构

在进行“Code Review”的时候,如果发现代码难以理解,这正是重构代码的良好契机。评审代码的时候一般会有一个评审者和一个原作者,在极限编程中叫做“结对编程”,在这种讨论的场景下,我们更容易跳出程序细节,从整体设计和可读性上审视代码。

何时不该重构

如果代码过于混乱,甚至根本没有办法正常运行,这个时候与其花大力气重构,倒不如重写来的快。

一个折中的方法是,将大的软件系统拆成多个子模块或子系统,然后分别对这些子模块或子系统进行重构或重写。

还有一个情况是当项目工期临近终点的时候,如果时间来不及重构应该优先保证项目进度。

我们常常把待重构的任务称为“技术债务”。类比现实生活中的债务,一个公司难免会有债务,而债务是要偿还的。当一个公司财务状况良好的时候,应该让自己的债务保持在一个健康的水平。但是当一个公司陷入泥潭,需要资金救急的时候,如果它手里有一笔钱,相比于还债,这笔钱更应该花在保证公司生存的事务上。


重构的难题

重构数据库

重构经常出问题的一个领域就是数据库。绝大多数程序都与它背后的数据库结构紧密耦合在一起,甚至还涉及到数据迁移,这都是数据结构如此难以重构的原因。

在非对象数据库中,解决这个问题的方法之一就是在对象模型和数据库模型之间插入一个分隔层。升级某个模型只需要修改分隔层即可,不需要改另一个模型。分割层会增加系统复杂度,但是可以也可以带来很大的灵活度。

重构接口

许多重构都需要修改接口,如果某个接口的调用者都在你的控制之下,那么修改接口就相对较为简单。

但是大多数情况下,你都需要维护新旧两个接口。


重构与设计

间接层

All problems in computer science can be solved by another level of indirection, except of course for the problem of too many indirections.

——David Wheeler
复制代码

这是计算机科学中的一句名言,翻译过来就是:计算机科学中的所有问题都可以通过另一个间接层次来解决,当然,间接层次太多的问题除外。

重构往往需要引入更多的间接层,把大对象拆成多个小对象,大函数拆成许多小函数。但你应该认清的是,间接层是一把双刃剑。每次引入一些间接层,带来的成本就是需要多管理一些东西。

虽然间接层可能会增加系统的复杂度,但是间接层也具有某些价值。

  1. 允许逻辑共享。
  2. 分开解释意图和实现。
  3. 隔离变化。
  4. 封装条件逻辑。

重构一方面要引入有价值的间接层,另一方面也要移除毫无价值的“寄生间接层”。


明天价值

程序的价值分为两个方面,即“今天能为你做什么”和“今后能为你做什么”。我们常常只关注前者,这就造成了程序“我今天能为你工作,明天我将完全无法胜任工作”的困境。

需要重构的代码往往都是难以修改的:

  1. 难以阅读。
  2. 逻辑重复。
  3. 添加新的行为需要修改已有代码。
  4. 带有复杂条件逻辑。

重构以后的代码需要达到的目标是:

  1. 容易阅读。
  2. 相同的逻辑只出现一次。
  3. 新的改动不会危及现有行为。
  4. 尽可能简单地表达条件逻辑。

性能提升

对于大多数程序而言,超过大半的时间都花费在一小半代码上。如果你要提高程序的性能,如果把时间花在那些不经常运行的代码上,对提高程序的整体性能影响不大。因此,重构要着重修改耗时最多的代码。