做当前这个项目也快一年半了,回头看看,前一年时间是在做重构,而后一年时间则是在打造一个新的产品。这里稍微总结一下做重构时所学到的一些东西吧。

重构其实可以是不同目标的,有些人重构是为了让代码更合理,美观;而另一些人则可能是为了实现某个功能;重构也是有不同程度的,有的可能只是在函数、类级别做些修改,而有些则是要对整个的架构,模块做变动;同时重构的投入也是有很大不同的,有的只是在遇到不好的代码或设计的时候才进行修正,而有的则会专门组建一个团队花一大段时间来做重构。

对基本概念的精确定义是一切讨论的基础,我这里讨论的重构是"为了实现某个功能专门进行的大规模的代码改动"。

架构设计,你能看的多远

简单一点来说,我们要做的,就是把一个软件的UI代码与核心功能彻底分开,然后把核心部分做成一个单独的产品。当然,这种所谓的表现层与业务层要分开的道理是谁都懂的,当初的架构里也的确加入了这些概念,但是由于没有严格要求,也从来不会把核心部分单独拿出来跑,经过近十年的开发,代码中核心层对UI的依赖已经相当严重,有静态的,源代码编译上的依赖,也有动态的,运行时的依赖。这个时候要抽取其核心功能,无疑是相当困难和费时的(代码量以百万行记)。看看现在网上的一些开源CAD软件,很多就是一开始就有明确的Core-UI划分,并且可以运行在核心模式或者UI模式。如FreeCAD。想想如果当年的架构师能够想到这一步,从一开始就明确划分,可以肯定的是:一、后期无需花费那么多人力物力;二、其质量,设计会好很多。

当然,这个其实也不一定是远见不够,一旦和商业利益结合起来考虑,很多好的设计是不得不被放弃的。举个例子,你的产品只是面向Windows用户的,而项目组里都是Windows程序员 - 为了更快更好的推出产品,你应该不会考虑跨平台吧 - 可是十年后,老大们决定向Mac进军了~~~所以说,这一块也是尽人事,听天命吧。

工作模式

一定要开出一个单独的branch来。这样你就可以关起门来"为所欲为"了,不会影响到其他team。这里为所欲为指的是:

  • Build Errors是允许的

       因为是很大的code base,你的某处修改可能在另外一处导致编译错误,或者你用script做的修改面特别广,而在本机做一个完整的build可能要半天(对的,即使用了IncrediBuild),那就check-in之后让服务器帮忙去build,你可以继续工作了,有几个build error,没关系!

  • Regression是正常的

每次check-in之前,不用跑那些自动化测试之类的。

当然,这种自由在很多情况下是不提倡的,但在这里,却非常大的提高了效率。

另外,因为重构的改动量是非常大的,所以要经常的与main或者trunk branck进行sync,把单次变动控制在一个可以接受的范围内。

如何保证质量

上面讲到我们可以不跑自动化测试而check-in,那么如何保证质量呢。

首先,你一定要有自动化测试 - 基于代码的单元测试也好,基于script的功能测试也好,只要是自动化的,并且覆盖率足够那就ok了 - 在做重构,尤其是大规模重构的时候,没有自动化测试那简直就是找死。

因为我们是在自己的branch上工作,只要保证回到main/trunk的时候没有regression就可以了,中间是什么状态,我们要求不是很高。一般的做法是:

  • 每周会跑一下smoketest和一些相关的acceptance test,防止一些重大问题。
  • 每次和main/trunk sync之前,我们都会花大概4天左右的时间来做"automation triage" - 把所有的自动化测试的case都跑一遍,拿到report之后逐个分析 - 或者逐批分析,因为很多failure都是一样的。

这种做法极大的提高了效率 - 要知道,要把所有的case跑完,需要几十台服务器一起跑3天~~~

如何管理代码

重构涉及到很多文件的移动与分拆,需要注意两个地方:

  • 文件的历史信息不能断

      一个文件是怎么一步步改动过来的是非常重要的信息 - 你可以方便的查到谁在什么时候改过这个文件,怎么改的。移动,或者分拆文件是非常容易因为疏忽而丢失历史的操作。一定要正确的使用SCM工具来保持此信息,比如perforce里就要用intergrate,而不是简单的add。

  • 文件的对应关系不能乱

       这里涉及到从branch到main的intergration,你在branch上把一个文件移动了并做了修改,而在main上同样有人做了修改,做intergration的时候,你很容易丢失别人在main上的修改,因为其对应关系并没有被建立起来,也就无从merge了。我想不同的SCM工具应该都提供了解决方案的,比如perforce就可以在其branch spec中来说明其对应关系。

重构的方法

工作期间拜读过《重构----改善既有代码的设计》,上面讲了许多不错的改善设计的方法与步骤,但是基本没用上。因为关于设计,我们对哪种情况,如何修改之前都已做了研究并有了方案,而那些步骤感觉稍显罗嗦,不是很适用。Visual Assist提供了个重构的模块,在一般规模的代码里用用还可以,但是对于有很多个solution的代码就无能为力了。况且这些只是涉及到源代码的重构,我们还有工程/DLL的重构。

我们采取的方法是:针对不同的情况,写perl脚本来自动化一些任务。举个简单点的例子:我修改了一个方法的名字,脚本就会搜索所有的代码,自动check-out需要修改的文件,并替换新名字。记得当时写了许多perl脚本来自动化对perforce的调用,VS的调用,对代码,工程文件的修改等等。

一些细节

  • interface的使用
    我们的重构工作对于接口可以说是无所不用其极,而正是大量的使用接口才让这种已经耦合很紧的代码的UI-核心的分离成为可能。比如某个核心层的操作完成之后是调用UI的刷新代码,而把这个刷新操作移到外面又是相当困难的,此时用接口就是比较好的做法:
     
    pInterface->UpdateUI();
    当然还有一些其他的使用接口的方式,但归根结底都是为了分离实现。
  • 使用vsprops
    我们用的是Visual Studio,上百个项目都有其各自的设置,而其实很多设置都是差不多的,完全可以把那些一致的设置放到一个vsprops文件中,让每个vcproj文件都引用它。能在很到程度上提高一致性与简洁度。MSDN有其详细的介绍。 
  • 虚函数与rebuild。添加,删除虚函数,尤其是基类里的虚函数都会破坏原有的虚表,因此除非rebuild所有可能引用到的代码,不然就会产生很奇怪的函数调用,具体可以看这篇:关于虚函数那点破事

做这种大型软件的重构,让我学到比较多的是:

  • 面对一些大型的软件系统不会犯憷,会比较有自信。
  • 养成自动化的习惯,一些大量的手工操作,会很枯燥,很费时,做的很容易出错而且没有成就感,但是把目标转换一下:写个程序把工作自动化,上面那些问题是不是都没了:)