我相信每个接受过老项目的程序员可能都吐槽过“前人的代码都是屎”。一个已经有些年头的项目,几乎肯定可以看到——到处拷贝来拷贝去的代码,随处可见的拼写错误,头重脚轻的函数……再看一看当年的提交者,可能是公司里的元老,甚至是大boss,不禁心里暗暗的鄙视,怀疑是否自己进错了公司。
而你被分配到接管这坨“屎”一般的代码,并且要在上面添加更多的功能,每次的增删代码都让你如履薄冰,每次遇到原来代码里的bug都让你的发际线再次上扬。
终于有一天,你忍不住了,脑子里面满满都是一个念头——我要重写这个代码。然后你真的这么做了,花了整整一个晚上/天/星期的时间,把代码改成了你心中满意的模样。然后代码上线了:
Happy Ending:重构的代码获得了同事的交口称赞,大家纷纷夸你代码比以前好写多了。
Normal Ending:过了几个月,你发现重构的代码又不行了,加一个新功能费死劲了,于是你又在筹划下一次重构。
Bad Ending:重构的代码上线后,bug不断,老板夺命连环call让你连夜修补,你发现老代码这么写不是没有道理的。
这样的故事每个经手过老项目的程序员可能都多少有类似的体会,在我的职业生涯中也经历了屈指可数的几次重构,然而每一次的重构经历几乎都踩到了各种各样的坑。
你真的需要重构吗
在重构项目之前,一定要再三的问自己(和自己的组员)这个问题:我们真的需要重构吗?
重构项目,在只是重构的前提下,对于公司的收益来说是——0,因为你的产品的用户,他们并不会为你的重构行为来买账,对于他们来说,你的源代码写的好看与否根本无所谓,对他们重要的是产品本身有没有改进。对于公司来说,重构行为不但没有带来任何利益,反而消耗了程序员资源,对于公司来说是损失。
一个互联网产品的生命周期可能就只有短短的几年,放长一点看,现在写的代码可能过几年就会毫无用处,在这样的前提下,现有项目的重构,一定是建立在项目本身还十分有前景的基础上,这个项目将来还有多少潜力,值不值得去重构?如果这个产品本身并没有什么可做的了,那么是否还值得花时间去重构它?
为什么需要进行项目重构
每个项目重构的理由各不相同,但个人总结来主要是以下两点
原来的项目漏洞太多,或者稳定性太差,当前的框架很难彻底根治。
新的项目需求,原有的程序框架已经无法满足。
假设你的项目没有很多bug,稳定性也很好,或者暂时没有在现有框架下很难实现的新的需求,那么不建议进行项目重构。
我在上一家公司的SEM组工作时,经历的第一次重构,是将后台的竞价计算出的竞价的结果,由数据库的表(Table)存储改成了推送到队列系统(RabbitMQ)。后台竞价程序算出的竞价结果需要由另一个上传程序上传到Adwords等竞价平台,我们在过去的做法是在数据库建立了一张表,竞价程序将算出的新竞价存储在其中,上传程序则定期的去查询表中的新加入的记录,将其成批上传,并在上传后删除。那么为何要进行这次重构?
随着公司的投放的广告词增加,单一上传程序实例很难在短时间内上传所有的竞价,但是如果运行多个上传程序的实例,则会出现多个示例同时查询新加入竞价并上传,删除同一记录造成数据库死锁。(1. 原来的项目漏洞太多,或者稳定性太差,当前的框架很难彻底根治。)
新业务需求需要计算另一种格式的竞价,如果继续使用数据库表来存储,则要么需要对已有的表进行字段扩容/修改,要么建立新的表单。但是当时已经预见将来可能会支持更多格式的竞价,于是数据库表的存储方式将不再灵活。(2. 新的项目需求,原有的程序框架已经无法满足。)
重构项目
经过再三衡量,我们终于还是决定重构项目,恭喜你,你将有一段踩坑之旅。
重构项目之前
重构项目的第一步是要了解项目。
重构时最容易发生的一类错误是没有能够完全的将原来的功能忠实的重现出来。很多开发者并不是手头的项目的原作者,并且项目也经过了很长时间的迭代,当代码越滚越大的时候,几乎没有人(包括原作者和产品经理)能够完全了解项目到底包含了哪些内容。当你看到重构后的功能和原来一模一样,并且测试人员也没有测出问题的时候,说不定哪个猴年马月添加进来的特殊功能,悄悄的被你干掉了。等到上线后,这个特殊功能的用户突然发现功能没了,于是过来投诉。
重构ING —— 测试
如果说什么是重构中最重要的第一步, 我认为是测试。
如果原来的代码没有单元测试、集成测试,有条件的话一定要补充上。为什么测试如此重要?打一个比好,重构就好像对着一把老钥匙来配新钥匙,而测试代码则是老钥匙的模子,我们做出来的新钥匙要能够和这个模子全对上。这个模子越详细,则新钥匙可以正常开锁的概率越大。
回想我在过去的重构中出现的一次重大失误,便是在重构过程中,有一个原来的单元测试出现了错误,原本的断言是结果为NULL,但是我的结果是0,当时觉得可能两种结果都可以,于是错误的选择了将单元测试的结果“改正”,结果在代码上线后,0的结果造成了程序输出和之前相比大不相同。总结一下:1. 如果有集成测试,则这样的错误可以在上线之前发现。2. 应该相信原来的单元测试集,而不应该“想当然”的去认为自己重构的逻辑正确。
重构ING —— 分支
代码重构的过程中,一定不建议先删除代码全部重写。比较推荐的是先拷贝出一个新的函数/文件/文件夹,然后写全新的代码。为什么要这么做?
在写新代码的时候可以一边写一边参照原来的代码。
新代码的代码审查(Code Review)会比较干净。
项目管理工具(Git,SVN)的历史比较干净。
回到我上面说的由数据库的表(Table)存储改成了推送到队列系统(RabbitMQ)的重构,当时我的做法是,在竞价程序端,重新实现了输出的函数,使得竞价结果可以改为推送到队列系统。而在上传程序端,则重新实现了一个新的程序,只从消息队列中消费推送的消息,然后上传到Adwords等广告平台。原有的旧上传程序则没有改动丝毫。
重构项目的上线 —— 开关
稍微大一些的重构,我会比较推荐使用程序开关,使用一些控制参数来控制逻辑入口是用老代码还是新代码,这样在线上出现了问题,可以及时的调整控制参数,迅速的回滚到老的逻辑。
如果程序运行的结果本身就是不确定的,不容易看出重构的错误,甚至推荐在重构的入口处设置A/B测试,这样在线上让一部分流量先走重构后的逻辑,同时将新/老逻辑的流量标记成不同的测试bucket,可以在数据测量平台上看到新老代码的表现如何。如果新代码的表现合理,则可以不断加大新代码的流量覆盖,直到100%。
在我上面提到的重构中,我选择在竞价程序计算段创建了一个新的A/B测试,对照组采用将竞价结果写到数据库的方法,实验组则将竞价结果发送到消息队列。同时在生产环境中,旧的和新的上传程序都在同时运行。在刚上线的时候,我选择将1%的竞价结果推送到消息队列中,然后观察新的上传程序能否将消息队列中的消息消耗掉。同时,在产品的监视页面,对对照组和实验组的竞价结果进行分析,确认两个组的竞价结果并没有明显的差别。
总结
总结一下个人的重构心得,重构前是否必要,重构中做好测试、分支、开关。
作者:ChaosYang1987
https://www.cnblogs.com/chaosyang/p/are-you-ready-for-refactoring.html