由于内容较多,可能会分几篇文章来记录。这一系列文章将会总结归纳《重构:改善既有代码的设计》这本书的重点内容。
本篇文章,对应书的一、二章。从一个案例入手,讲述什么是重构,重构的关键概念,以及重构原则。
一个案例
通过一个影片租赁系统来介绍重构的缘由和一些基本概念。
- 通过案例,而不是理论来介绍新概念。
- 在需要添加新特性,却需要大量修改代码时,就需要进行重构
- 重构的第一步是增加良好的测试。
- 一些重构的方式:
- 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,使得每一步重构都确认不可能修改逻辑:
- 抽取价格获取函数,到新增的 Price 类(作为 Movie 的 state/strategy)中。在 Movie 中增加一个 Price 成员变量,将实际的价格计算转移到 Price 中。
- 为每一个 Movie 类型新增一个对应的继承 Price 的类。在设置影片类型时对应的创建 Price 子类。
之后,再通过 Replace Conditional with Polymorphism 将价格计算从 Price 类转移到 Price 子类中。
重构原则
何为重构
重构不应该改变代码逻辑。
所以,我们在开发过程中,经常会轮流戴着“两顶帽子”。一顶是新增特性,这时不应该修改任何现有代码,而只是增加代码和测试;另一顶是重构,这时,除非特殊情况,不应该修改测试和增加特性,而只是修改代码结构。
虽然重构经常出现在新增特性之时(只有这时我们才能清楚的意识到现有代码的问题),但我们脑海中要时刻记住这“两顶帽子”的角色切换。
为何重构
- 改进设计(基本上,设计的改善都是为了消除重复代码)
- 更易于理解。(我的感觉也是这样,重构实际上是为了让普通人也能理解、修改软件设计,并创造优秀设计的一种手段。当然,另一方面,聪明的人也能从重构中,获得更快理解设计的益处)
- 帮助调试。其实也是对程序逻辑梳理后能更好理解程序的一种收益。
- 提高编程速度。就是说良好设计有利于程序的扩展。
何时重构
- 类似事情重复做了三遍时。(简单的 extract method, extract xxx 能覆盖大部分重复情况)
- 添加新功能时。
- 改bug时
- Code Review 时
关于项目进度
重构的价值,是“明天可以为你做什么”。甚至有时候,重构就是最快的方式。
间接层
重构、重复代码的减少,都离不开“间接层”。通过一个间接层,来:
- 允许逻辑共享
- 分开“解释意图”和“实现”。通过类名、函数名来多一层“注释”,避免不必要的代码阅读。
- 隔离变化。
- 封装条件逻辑。用多态来代替条件,更易于扩展和公共代码的提取。
重构的难题
- 数据库。对数据库的重构涉及到 migration,是一件非常复杂又容易出错的事情。解决办法是引入分隔层,通过分隔层的修改来屏蔽数据库修改,当确认这个修改是稳定的之后,再做数据库修改。相关书籍参考:《数据库重构》
- 修改接口。首先需要做的是,控制发布接口数量。然后通过新增接口,让旧接口调用新接口,然后标记旧接口为 deprecated。
- 其他通过重构难以解决的问题。有些设计根本上的问题,重构是解决不了的。所以一个折中的做法是,设计时,发现是较好重构的结构时,使用最简单的设计。否则,多花些时间设计。
- 何时不该重构。有时候你需要重写,而不是重构。折中做法是先重构封装小模块,然后逐一重写。另外项目尾期,也要避免重构。
与程序设计互补
利用持续重构来减少提前设计。
也就是说,我们不需要因为花费大量时间和经历来试图产出一个“完美”的设计。而是在设计时只考虑以下问题:
“我目前的设计是不是能在软件需要扩展时,比较容易的进行重构?”
也就是说,只要设计的大方面没有问题,就可以大胆编码,随着功能的逐渐加入再持续的重构。通过这样的方式,可以极大的减小前期的设计压力。并且,不那么容易“过度设计”。
重构和性能
有人会认为重构虽然变得易于理解,但也引入了大量的中间层转换、很深的函数调用。作者认为,性能是一定需要考虑的问题,但重构有可能让性能提升更容易。因为重构后的代码责任清晰,更容易做“度量”,另一个经验是,性能瓶颈通常在一个非常特定代码段,只要针对它做性能优化就够了。
所以作者认为,除非是对性能要求极高的“实时系统”,大部分软件因为性能而不做重构都是过于狭隘了。
小结 - 我想说的
本书中提到的一些重构知识,已经在我的工作中进行了一些尝试。比如:
- 利用重构来阅读代码。在帮助自己理解代码的同时,通过一些修改命名、抽取函数、复用代码等等几乎完全不影响业务逻辑的重构,来顺带提升代码质量,是件非常有意义的事情。
- “两顶帽子”的角色转换。在缺少单元测试的遗留代码里,这样的转换似乎并不太有效,但还是能稍微清理自己的思路。当然,在做大的重构时(比如 Move 一个类到新的包等大幅改变代码的重构),一定不要增加新功能,单独提交代码,这对code review 和后期的问题追踪都是大有帮助的。
- 简化提前设计。当自己熟悉了IDE自带的各种重构快捷键后,重构所需的精力越来越少。以至于有的时候我故意的不做设计,将设计交给重构(比如我经常用 extract method 来帮助我编写新函数 -_-)。因为工具重构时,能自动帮你检查依赖关系,比自己写保险多了。。
这两章对重构这一概念做了入门介绍。后文通过描述什么是不好的代码,有哪些重构方法,具体手法如何进行了详细介绍。
Comments