开发工程师应该都遇到过以下几种情况:
接收新项目时,发现项目代码一团糟,很多地方代码大量重复、各种反模式、类或者接口职责非常不清晰、几百行的方法等
自己参与开发的项目,随着进度的推进,人员变更,需求的不断加入,使即使是最初设计整个架构的开发人员都很难理清楚项目,项目代码变得风格多样,杂乱无章,bug频出。新加功能越来越难,开发人员也在一直在埋怨这个地方设计的不好、那个地方为什么要这么做等等,项目代码里出现很多上面提到的问题。
到目前为止,我直接或者间接参与的项目为7个,上述问题都是真实遇到的,平常在开发或者参与代码评审时一直在朝着这方面努力,尝试解决这个问题,心里也一直隐隐有个概念:这必定是软件工程中一个已经被定义并且有相应方法论的问题。 直到看到了《重构》一书,恍然大悟,许多平时尝试过或者思考了而没有实践的东西在里面都清清楚楚列着,很多例子都是似曾相识。
这篇笔记就是结合自己的理解,记录的一些基本概念,但是肯定是管中窥豹,很多具体重构手段或者说细节需要如下几方面的积累才能有真正的自己的想法:
项目经验
软件工程基础概念,设计模式,oo设计原则等
结合开发过程的思考
这些知识既需要项目实践,也需要阅读。比较推荐的两本书是《重构》,《代码大全》。
为何重构
改进设计
使软件更易于理解
帮助找到bug
何时重构
重构不应该是专门安排时间来做,相反,随时随地都可以进行,不应该为了重构而重构,当遇到想做某件事,而你发现重构可以帮助你把这些事做得更好或者更快
- 三次法则
第一次尽管去做,第二次做类似的反感,但还是可以去做,第三次再做类似得事,就应该重构
- 添加功能时重构
随着项目代码的不断增加,新的需求不断叠加到老的设计或者说代码上时,某个时刻你会发现现有的设计无法帮助你轻松的添加需要的功能,但你想到,如果采用某种方式的设计,那添加功能会容易得多 —— 那这就是重构的时机。它应该可以带来至少如下两方面的好处:
- 未来添加新特性更容易
- 是弥补过去的错误最为快捷的方法
- 修改错误时重构
调试过程中运用重构,多半是为了使代码更具可读性。还有种情况就是收到一个bug,而你无法一眼看出根源,对于有Java这类语言,通常就意味着需要重构已得改进代码的清晰度了
- 代码审查时重构
很多公司都有常规的代码审查,他的意义主要应该在控制项目代码质量。通常情况下,是有有经验的开发者来完成这一工作,他们往往对于代码编写有着更多的理解,能够看出代码里一些不合理、表现不够优异的代码并给出修改意见。 另外一方面,开发人员往往对于自己编写的代码感到清晰,但事实是否如此-代码审查就可以证明这一点。 审查-更改这一过程,其实就是重构的一个被动发生。
重构基本原则
重构是这样一个过程:在不改变代码外部行为的情况下,对代码做出修改。已改进程序的内部结构。本质上说,重构就是在代码写好后改进他的设计。
按照软件开发的理解,我们应该是先有个良好的设计,然后才能开始编码。 但是,随时时间流逝,需求的更改,人员的变更,代码的不断修改,根据原先设计所得的系统的代码逐渐沉沦,越来越难以维护。
重构与此相反,通过一些很简单的步骤来逐步改善代码:
- 移动某个字段到另一个类
- 把某些代码装成函数
- 继承体系中挪动函数
重构的第一步总是为即将修改的代码建立一组可靠的测试,好的测试是重构的基础,花时间建立一个优良的测试机制是完全值得的。
基本重构条件
那么在哪些情况下需要重构呢? 或者说看到了怎样的代码时就意味着可以重构呢。这里列出一些非常常见的情况,更多的方法需要参考书中。
duplicated code
重复代码
在使用IDEA进行编程的时候,对于重复代码默认它会使用波浪线标识出来。 所以如果看到这样的代码:设法将他们合二为一。
最简单的情况就是同一个类中某两个方法有相同的代码段, 这时可以采用Extract Method
方式将他们提炼为一个独立的方法来处理。
另外一种常见的就是某些兄弟类(Java中实现了同一个接口的类)包含相同表达式,这种情况只需要对他们分别Extract Method
,然后再对提炼出来的方法Pull Up Method
到父类。 如果他们的实现并非完全相同,那么就得运用Extract Method
将相似部分和差异部分隔离出来,然后将相似部分提炼到父类,而差异部分由子类实现-这通常意味着使用了模板模式(Template Method
)
如果两个完全无关的类出现了相同代码段,应该考虑提炼其中一个的代码段到一个独立类中,然后让另外一个类调用该提炼出来的类。但是,提炼出来的位置并非一定要是一个新类- 这个需要结合具体的场景才能确定。 一般来说, 组件类、工具类、服务类是选择。
Long Method
过长函数
毫无疑问, 随着业务的增长、代码量的累计,过长函数极有可能出现在很多项目中。而过长的方法带来的维护性问题相信稍有经验的都知道是多么的可怕。
好在,百分之九十的情况下,要把函数变小,只需要找到函数中适合在一起的代码段,然后Extract Method
。如果函数内有大量的临时变量或者参数,则可以运用Replace Temp With Query
、Introduce Parameter Object
和Preserve Whole Object
等操作来处理。
条件表达式和循环往往也是可以提炼出来,循环条件和其内的代码可以独立为一个函数。
Large Class
过大的类
如同上面过长函数一样, 过大的类也往往意味着维护难度的成倍增加。 通常情况下,一个过大的类都会或多或少的违反六大设计基本原则的单一职责原则: 做了过多的事情。
可以运用Extract Method
把几个变量一起提炼至新类中,提炼时应该选择类内彼此关联的变量,将他们放在一起。通常如果类中数个变量有相同的开始或者结尾,就意味着就机会把他们提炼到单独的某个组件中去。
Long Parameter LIst
过长参数列
太长的参数列表难以理解,并且调用者很容易传错参数。
如果向已有的对象调用某个方法就可以取代另外一些参数,那么可以采用Replace Parameter With Method
,这里已有的对象即可以是当前类的字段也可以是另外一个参数。 还可以运用Preserve Whole Object
将来自同一个对象的一堆数据收集起来,并以该对象替换他们。 如果某些参数缺乏合理的归属对象,则可以为他们专门建立一个参数对象来处理。 这几种手法在Java Web项目中常常可以表现为生成某个BO, VO, DTO等等
Divergent Change
发散式变化
如果某个类经常因为不同的原因在不同的方向上发生变化,Divergent Change
就出现了。比如面对某个类时,发现:“如果我要新加入一个数据库,那么我要改3个函数;如果新出现一种金融渠道,我需要改这4个函数”,那么此时把这个对象分为两个不同职责的对象也许会更好-单一职责原则。 针对某一外界条件变化所引起的所有修改,都应该只发生在单一类中,而这个类中的所有内容都应该反应此变化。
Shotgun Surgery
-霰弹式修改
如果遇到某种变化,需要很多不同的地方作出许多小修改-这也意味着需要重构。
这种情况应该使用Move Method
和Move FIeld
把所有需要改动的代码放到一个类中,如果眼下没有合适的就为此专门创建一个。Shotgun Surgery
和Divergent Change
的最终目地都是一样的:使外界变化和需要修改的类趋于一一对应。Data Clumps
数据泥团
在很多地方看到相同的三四项数据: 两个类中相同的字段、很多函数签名中相同的参数。 这些总是绑定在一起的数据应该有属于他们自己的对象。
首先找出这些数据以字段形式出现的地方,运用Extract Class
把他们提炼到一个独立对象中去。
Extract Method
提炼函数
某些方法里动辄上百行 - 大部分情况下是没有必要并且难以维护的。那么将某部分上下文关联不强(也就是引用的局部变量不多、上下文关系不复杂)的代码片段抽象为单独的方法就是一个很有效率的优化方式。 现代IDE对这个都提供了良好的支持,可以非常方便、不引入问题的情况下轻松完成这个更改。
Pull Up/Down Method
挪动方法
对于一些按照现代OO思维编写的很多代码中,类与类之间的沟通无非是继承、组合,当然,按照设计模式来说,组合由于继承。 在一个层次较复杂,类数量较多的体系中,如果某几个处于某个相同层次(既可以从业务商衡量,比如都是完成的这一层级的功能,也可以从技术上衡量,比如都是对service提供服务但是又高于Dao),并且有重复代码时,就可以采用Pull up/down Method
来处理。 比如把某两个Service
的方法下沉到Manager
中或者把某个Manager
中方法上浮到Service
中,这样可以明显的减轻代码量。 一个明显的例子比如在某些Mock
代码中,基于不同环境选用Mock
实现类,那么Mock
实现类中就仅仅应该包含必须更改的逻辑部分,其他的逻辑都应该下沉到父类中处理。