由于内容较多,可能会分几篇文章来记录。这一系列文章将会总结归纳《重构:改善既有代码的设计》这本书的重点内容。

本篇文章,对应书的一、二章。从一个案例入手,讲述什么是重构,重构的关键概念,以及重构原则。

一个案例

通过一个影片租赁系统来介绍重构的缘由和一些基本概念。

  1. 通过案例,而不是理论来介绍新概念。
  2. 在需要添加新特性,却需要大量修改代码时,就需要进行重构
  3. 重构的第一步是增加良好的测试。
  4. 一些重构的方式:
    • Extract Method, Extract Variable, Move Method
    • Replace Temp with Query
      不要担心性能损耗,因为这很容易优化
    • Form Template Method
      重构以后的结果有些工厂方法的感觉,书里说,这是 Template Method 模式
    • Self Encapsulate Field
    • Replace Type Code with State/Strategy
      将类型码转换成类型类,通过继承来表示多个类型的逻辑
    • Replace Conditional with Polymorphism
      当需要使用条件判断时(switch),将判断条件抽取成子类,通过多态来确定执行逻辑。

案例最后的一次重构较为复杂。

由于不同类型的影片具有不同的价格策略。原来的程序通过switch语句结合影片类型码进行了影片价格的计算。

重构时,通过 Replace Type Code with State/Strategy 和 Replace Conditional with Polymorphism 将影片价格的计算移动到一个新增的 Price 类的子类中。

并且,文章通过一种比较巧妙的方式来实现 Replace Type Code with State/Strategy,使得每一步重构都确认不可能修改逻辑:

  1. 抽取价格获取函数,到新增的 Price 类(作为 Movie 的 state/strategy)中。在 Movie 中增加一个 Price 成员变量,将实际的价格计算转移到 Price 中。
  2. 为每一个 Movie 类型新增一个对应的继承 Price 的类。在设置影片类型时对应的创建 Price 子类。

之后,再通过 Replace Conditional with Polymorphism 将价格计算从 Price 类转移到 Price 子类中。

重构原则

何为重构

重构不应该改变代码逻辑。

所以,我们在开发过程中,经常会轮流戴着“两顶帽子”。一顶是新增特性,这时不应该修改任何现有代码,而只是增加代码和测试;另一顶是重构,这时,除非特殊情况,不应该修改测试和增加特性,而只是修改代码结构。

虽然重构经常出现在新增特性之时(只有这时我们才能清楚的意识到现有代码的问题),但我们脑海中要时刻记住这“两顶帽子”的角色切换。

为何重构

  1. 改进设计(基本上,设计的改善都是为了消除重复代码)
  2. 更易于理解。(我的感觉也是这样,重构实际上是为了让普通人也能理解、修改软件设计,并创造优秀设计的一种手段。当然,另一方面,聪明的人也能从重构中,获得更快理解设计的益处)
  3. 帮助调试。其实也是对程序逻辑梳理后能更好理解程序的一种收益。
  4. 提高编程速度。就是说良好设计有利于程序的扩展。

何时重构

  1. 类似事情重复做了三遍时。(简单的 extract method, extract xxx 能覆盖大部分重复情况)
  2. 添加新功能时。
  3. 改bug时
  4. Code Review 时

关于项目进度

重构的价值,是“明天可以为你做什么”。甚至有时候,重构就是最快的方式。

间接层

重构、重复代码的减少,都离不开“间接层”。通过一个间接层,来:

  1. 允许逻辑共享
  2. 分开“解释意图”和“实现”。通过类名、函数名来多一层“注释”,避免不必要的代码阅读。
  3. 隔离变化。
  4. 封装条件逻辑。用多态来代替条件,更易于扩展和公共代码的提取。

重构的难题

  1. 数据库。对数据库的重构涉及到 migration,是一件非常复杂又容易出错的事情。解决办法是引入分隔层,通过分隔层的修改来屏蔽数据库修改,当确认这个修改是稳定的之后,再做数据库修改。相关书籍参考:《数据库重构》
  2. 修改接口。首先需要做的是,控制发布接口数量。然后通过新增接口,让旧接口调用新接口,然后标记旧接口为 deprecated。
  3. 其他通过重构难以解决的问题。有些设计根本上的问题,重构是解决不了的。所以一个折中的做法是,设计时,发现是较好重构的结构时,使用最简单的设计。否则,多花些时间设计。
  4. 何时不该重构。有时候你需要重写,而不是重构。折中做法是先重构封装小模块,然后逐一重写。另外项目尾期,也要避免重构。

与程序设计互补

利用持续重构来减少提前设计。

也就是说,我们不需要因为花费大量时间和经历来试图产出一个“完美”的设计。而是在设计时只考虑以下问题:

“我目前的设计是不是能在软件需要扩展时,比较容易的进行重构?”

也就是说,只要设计的大方面没有问题,就可以大胆编码,随着功能的逐渐加入再持续的重构。通过这样的方式,可以极大的减小前期的设计压力。并且,不那么容易“过度设计”。

重构和性能

有人会认为重构虽然变得易于理解,但也引入了大量的中间层转换、很深的函数调用。作者认为,性能是一定需要考虑的问题,但重构有可能让性能提升更容易。因为重构后的代码责任清晰,更容易做“度量”,另一个经验是,性能瓶颈通常在一个非常特定代码段,只要针对它做性能优化就够了。

所以作者认为,除非是对性能要求极高的“实时系统”,大部分软件因为性能而不做重构都是过于狭隘了。

小结 - 我想说的

本书中提到的一些重构知识,已经在我的工作中进行了一些尝试。比如:

  • 利用重构来阅读代码。在帮助自己理解代码的同时,通过一些修改命名、抽取函数、复用代码等等几乎完全不影响业务逻辑的重构,来顺带提升代码质量,是件非常有意义的事情。
  • “两顶帽子”的角色转换。在缺少单元测试的遗留代码里,这样的转换似乎并不太有效,但还是能稍微清理自己的思路。当然,在做大的重构时(比如 Move 一个类到新的包等大幅改变代码的重构),一定不要增加新功能,单独提交代码,这对code review 和后期的问题追踪都是大有帮助的。
  • 简化提前设计。当自己熟悉了IDE自带的各种重构快捷键后,重构所需的精力越来越少。以至于有的时候我故意的不做设计,将设计交给重构(比如我经常用 extract method 来帮助我编写新函数 -_-)。因为工具重构时,能自动帮你检查依赖关系,比自己写保险多了。。

这两章对重构这一概念做了入门介绍。后文通过描述什么是不好的代码,有哪些重构方法,具体手法如何进行了详细介绍。