如何改善遗留的代码库
在每一个程序员、项目管理员、团队领导的一生中,这都会至少发生一次。原来的程序员早已离职去度假了,给你留下了一坨几百万行屎一样的、勉强支撑公司运行的代码和(如果有的话)跟代码驴头不对马嘴的文档。
你的任务:带领团队摆脱这个混乱的局面。
当你的第一反应(逃命)过去之后,你开始去熟悉这个项目。公司的管理层都在关注着你,所以项目只能成功;然而,看了一遍代码之后却发现失败几乎是不可避免。那么该怎么办呢?
幸运(不幸)的是我已经遇到好几次这种情况了,我和我的小伙伴发现将这坨热气腾腾的屎变成一个健康可维护的项目是一个有丰厚利润的业务。下面这些是我们的一些经验:
备份
在开始做任何事情之前备份与之可能相关的所有文件。这样可以确保不会丢失任何可能会在另外一些地方很重要的信息。一旦修改了其中一些文件,你可能花费一天或者更多天都解决不了这个愚蠢的问题。配置数据通常不受版本控制,所以特别容易受到这方面影响,如果定期备份数据时连带着它一起备份了,还是比较幸运的。所以谨慎总比后悔好,复制所有东西到一个绝对安全的地方并不要轻易碰它,除非这些文件是只读模式。
重要的先决条件:必须确保代码能够在生产环境下构建运行并产出
之前我假设环境已经存在,所以完全丢了这一步,但 Hacker News 的众多网友指出了这一点,并且事实证明他们是对的:第一步是确认你知道在生产环境下运行着什么东西,也意味着你需要在你的设备上构建一个跟生产环境上运行的版本每一个字节都一模一样的版本。如果你找不到实现它的办法,一旦你将它投入生产环境,你很可能会遭遇一些预料之外的糟糕事情。确保每一部分都尽力测试,之后在你足够确信它能够很好的运行的时候将它部署生产环境下。无论它运行的怎么样都要做好能够马上切换回旧版本的准备,确保日志记录下了所有情况,以便于接下来不可避免的 “验尸” 。
冻结数据库
直到你修改代码结束之前尽可能冻结你的数据库,在你已经非常熟悉代码库和遗留代码之后再去修改数据库。在这之前过早的修改数据库的话,你可能会碰到大问题,你会失去让新旧代码和数据库一起构建稳固的基础的能力。保持数据库完全不变,就能比较新的逻辑代码和旧的逻辑代码运行的结果,比较的结果应该跟预期的没有差别。
写测试
在你做任何改变之前,尽可能多的写一些端到端测试和集成测试。确保这些测试能够正确的输出,并测试你对旧的代码运行的各种假设(准备好应对一些意外状况)。这些测试有两个重要的作用:其一,它们能够在早期帮助你抛弃一些错误观念,其二,这些测试在你写新代码替换旧代码的时候也有一定防护作用。
要自动化测试,如果你有 CI 的使用经验可以用它,并确保在你提交代码之后 CI 能够快速的完成所有测试。
日志监控
如果旧设备依然可用,那么添加上监控功能。在一个全新的数据库,为每一个你能想到的事件都添加一个简单的计数器,并且根据这些事件的名字添加一个函数增加这些计数器。用一些额外的代码实现一个带有时间戳的事件日志,你就能大概知道发生多少事件会导致另外一些种类的事件。例如:用户打开 APP 、用户关闭 APP 。如果这两个事件导致后端调用的数量维持长时间的不同,这个数量差就是当前打开的 APP 的数量。如果你发现打开 APP 比关闭 APP 多的时候,你就必须要知道是什么原因导致 APP 关闭了(例如崩溃)。你会发现每一个事件都跟其它的一些事件有许多不同种类的联系,通常情况下你应该尽量维持这些固定的联系,除非在系统上有一个明显的错误。你的目标是减少那些错误的事件,尽可能多的在开始的时候通过使用计数器在调用链中降低到指定的级别。(例如:用户支付应该得到相同数量的支付回调)。
这个简单的技巧可以将每一个后端应用变成一个像真实的簿记系统一样,而像一个真正的簿记系统,所有数字必须匹配,如果它们在某个地方对不上就有问题。
随着时间的推移,这个系统在监控健康方面变得非常宝贵,而且它也是使用源码控制修改系统日志的一个好伙伴,你可以使用它确认 BUG 引入到生产环境的时间,以及对多种计数器造成的影响。
我通常保持每 5 分钟(一小时 12 次)记录一次计数器,但如果你的应用生成了更多或者更少的事件,你应该修改这个时间间隔。所有的计数器公用一个数据表,每一个记录都只是简单的一行。
一次只修改一处
不要陷入在提高代码或者平台可用性的同时添加新特性或者是修复 BUG 的陷阱。这会让你头大,因为你现在必须在每一步操作想好要出什么样的结果,而且会让你之前建立的一些测试失效。
修改平台
如果你决定转移你的应用到另外一个平台,最主要的是跟之前保持一模一样。如果你觉得需要,你可以添加更多的文档和测试,但是不要忘记这一点,所有的业务逻辑和相互依赖要跟从前一样保持不变。
修改架构
接下来处理的是改变应用的结构(如果需要)。这一点上,你可以自由的修改高层的代码,通常是降低模块间的横向联系,这样可以降低代码活动期间对终端用户造成的影响范围。如果旧代码很庞杂,那么现在正是让它模块化的时候,将大段代码分解成众多小的部分,不过不要改变量和数据结构的名字。
Hacker News 的 mannykannot 网友指出,修改高层代码并不总是可行,如果你特别不幸的话,你可能为了改变一些架构必须付出沉重的代价。我赞同这一点也应该在这里加上提示,因此这里有一些补充。我想额外补充的是如果你修改高层代码的时候修改了一点点底层代码,那么试着只修改一个文件或者最坏的情况是只修改一个子系统,尽可能限制修改的范围。否则你可能很难调试刚才所做的更改。
底层代码的重构
现在,你应该非常理解每一个模块的作用了,准备做一些真正的工作吧:重构代码以提高其可维护性并且使代码做好添加新功能的准备。这很可能是项目中最消耗时间的部分,记录你所做的任何操作,在你彻底的记录并且理解模块之前不要对它做任何修改。之后你可以自由的修改变量名、函数名以及数据结构以提高代码的清晰度和统一性,然后请做测试(情况允许的话,包括单元测试)。
修复 bug
现在准备做一些用户可见的修改,战斗的第一步是修复很多积累了几年的 bug。像往常一样,首先证实 bug 仍然存在,然后编写测试并修复这个 bug,你的 CI 和端对端测试应该能避免一些由于不太熟悉或者一些额外的事情而犯的错误。
升级数据库
如果你在一个坚实且可维护的代码库上完成所有工作,你就可以选择更改数据库模式的计划,或者使用不同的完全替换数据库。之前完成的步骤能够帮助你更可靠的修改数据库而不会碰到问题,你可以完全的测试新数据库和新代码,而之前写的所有测试可以确保你顺利的迁移。
按着路线图执行
祝贺你脱离的困境并且可以准备添加新功能了。
任何时候都不要尝试彻底重写
彻底重写是那种注定会失败的项目。一方面,你在一个未知的领域开始,所以你甚至不知道构建什么,另一方面,你会把所有的问题都推到新系统马上就要上线的前一天。非常不幸的是,这也是你失败的时候。假设业务逻辑被发现存在问题,你会得到异样的眼光,那时您会突然明白为什么旧系统会用某种奇怪的方式来工作,最终也会意识到能将旧系统放在一起工作的人也不都是白痴。在那之后。如果你真的想破坏公司(和你自己的声誉),那就重写吧,但如果你是聪明人,你会知道彻底重写系统根本不是一个可选的选择。
所以,替代方法:增量迭代工作
要解开这些线团最快方法是,使用你熟悉的代码中任何的元素(它可能是外部的,也可能是内核模块),试着使用旧的上下文去增量改进。如果旧的构建工具已经不能用了,你将必须使用一些技巧(看下面),但至少当你开始做修改的时候,试着尽力保留已知的工作。那样随着代码库的提升你也对代码的作用更加理解。一个典型的代码提交应该最多两三行。
发布!
每一次的修改都发布到生产环境,即使一些修改不是用户可见的。使用最少的步骤也是很重要的,因为当你缺乏对系统的了解时,有时候只有生产环境能够告诉你问题在哪里。如果你只做了一个很小的修改之后出了问题,会有一些好处:
- 很容易弄清楚出了什么问题
- 这是一个改进流程的好位置
- 你应该马上更新文档展示你的新见解
使用代理的好处
如果你做 web 开发那就谢天谢地吧,可以在旧系统和用户之间加一个代理。这样你能很容易的控制每一个网址哪些请求定向到旧系统,哪些请求定向到新系统,从而更轻松更精确的控制运行的内容以及谁能够看到运行系统。如果你的代理足够的聪明,你可以使用它针对个别 URL 把一定比例的流量发送到新系统,直到你满意为止。如果你的集成测试也能连接到这个接口那就更好了。
是的,但这会花费很多时间!
这就取决于你怎样看待它了。的确,在按照以上步骤优化代码时会有一些重复的工作步骤。但是它确实有效,而这里介绍的任何一个步骤都是假设你对系统的了解比现实要多。我需要保持声誉,也真的不喜欢在工作期间有负面的意外。如果运气好的话,公司系统已经出现问题,或者有可能会严重影响到客户。在这样的情况下,我比较喜欢完全控制整个流程得到好的结果,而不是节省两天或者一星期。如果你更多地是牛仔的做事方式,并且你的老板同意可以接受冒更大的风险,那可能试着冒险一下没有错,但是大多数公司宁愿采取稍微慢一点但更确定的胜利之路。
via: https://jacquesmattheij.com/improving-a-legacy-codebase
作者:Jacques Mattheij 译者:aiwhj 校对:JianqinWang, wxy