【CSDN编者按】Stack Overflow创始人Joel Spolsky发布过一篇很有名的文章,他的观点是永远不要从零开始重写一段代码。他引用了Netscape的例子,他们花费好多年重写软件,结果在重写过程中公司就倒闭了。但是,本文作者已然选择从零开始重写应用程序,其中的原因是什么呢?
作者 | Nicholas Tietz-Sokolsky以下为译文:
大概一年多以前,我重读了这篇文章(
https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i/),但我依然选择了从零开始重写我们的应用程序。在本文中,我将简单介绍一下事情的经过,我们成功重写代码的过程,以及从中吸取的经验教训。
事情发生在2019年1月,当时Remash还是一家很小的公司。我们雇佣了几名工程师,一共有5个人负责产品,还有一些工程师负责机器学习或开发运维。尽管雇佣了许多工程师,我们的开发速度依然很慢。即使是添加很简单的功能也要花很长时间。我们的产品中有许多bug,我们只能标记为“已知”而无力修复。而整个产品像是很久没有任何新改进一样。
重要的是要理解为什么会出现这些问题。我们假设(并在重写之后验证了这个假设)这些问题并不是人员的问题,我们雇佣的都是很强力的工程师。问题主要出在代码和过程上。旧的代码很差,因为我们以前的技术力不高,解决问题的方式也不好;而我们的过程会鼓励并依赖于孤岛的知识,因为我们没有真正的“全栈”。
2019年1月代码的状态
旧的应用程序的设计目的与现在有很大差异。最初,Remesh的目的是在两个组或个人与组之间建立双向的对话。例如,你可以让民主党和共和党互相对话来理解互相的观点并求同存异。或者,可以让市场与市民对话以理解市民的需要和观点。但是,在调查了产品与市场的契合度之后,我们改变了用例。我们开始着力于一个人与一组人的对话。
由于这个变化,一些旧的设计决策不再适用,数据库结构也需要大幅改动。除了数据库问题之外,代码本身也很难理解,因为一直在添加新功能,而没有做过大的重构。最需要重构的地方,测试覆盖率很低,因为那些都是最古老的代码,都是在我们建立良好的测试实践之前编写的。
除了这些之外,语言和框架也并不适合团队。后台代码采用了Elixir,只有很少几名工程师理解。前端代码之一完全是使用旧版本的Angular编写的,我们还有另外两个前端,使用的是React。我们几乎没有工程师能精通一种技术,更别说三种了。语言和框架并不适合我们的团队,也不适合要解决的问题,这大大影响了我们的开发速度。
有什么选择?
当时,我们知道代码需要大的改变。在面对一堆很棘手的代码时,你的选择只有三个:
对于前端来说,重构并不可行。Angular的版本太老了,甚至都找不到合理的路径升级到现代的Angular(实际上我们也不想使用任何版本的Angular)。由于我们的UI和API经历了重大变更,因此重构并不可行。所以对于前端而言,我们的选择只能是一次性重写,或者一点点重写。后端有几个我们想解决的问题:数据库结构、语言以及那些无法再解决问题的代码。我们使用Elixir的原因是它强大的并发支持,但我们根本没有用到并发,所以它带来的只有痛苦——由于它在Erlang VM中处理并发的方式,导致性能测试非常困难,你只能看出做了什么,而无法看出从哪里调用。所以,性能优化只能是碰运气。Elixir的代码也限制了机器学习工程师对于后台代码的贡献程度,因为他们日常使用的是Python,没有时间深入学习Elixir。长话短说,我们希望放弃Elixir,完全迁移至Python,这样整个团队都可以贡献力量,该语言能支持所有问题,性能测试也更容易一些。我们还有一些“产品债务”,自于一些用户接受但不是太理想的概念。这些都是局限范围内的最优解。如果能打破这个局限并选择最好的方法,我们也许会一步到位。去掉这些不理想的概念需要一次性做很多事情。- 我们希望团队每个成员都能为后端贡献代码,因此最合适的语言是Python,它易习易用,而且在团队内有广泛的接受度。
- 旧的代码太不稳定,测试也不全面,因此重构风险很大。
- 采用一个有强烈意见的框架(如Django)可以获得效率,还能节省许多时间(如Django Admin)。
- 我们有机会根据从客户那里收集到的信息制作一个全新的版本,而不必让每个客户都经历大量的小改动。这样客户培训也很容易,销售团队也只需要努力一次就可以,而不需要一直向客户灌输新概念。
为了做出这个决定,我们做了非常详尽的计划。尽管人们现在说的都是敏捷开发,但在这个问题上,我们采用了瀑布式的方法,不仅因为我们要用瀑布方式来实现,而且因为要找出每种方案需要付出的代价是什么。很明显,一次性重写整个应用程序会花费许多时间,但重构或一点点重写会花费更多时间,而且有更大的不确定性。选择重构的风险要大得多。最后,我们对于这个决定十分有信心,并且说服了公司的所有人。我们决定要重写,因为这样做可以改正多年来的错误,同时可以让产品前进一大步。
在确定了需要包括哪些功能后,我们从2019年2月开始重写。所以我们的计划非常周密,特别是在需要做什么方面。这个计划并不敏捷,但做好计划,可以帮助我们一直朝着正确的方向前进。因为我们曾在用户测试阶段偏离过方向,而且在那之后偏离得越来越多。在度过了坎坷的开头后,新版本的实际构建过程非常顺利。让所有人都切换到新的技术栈很痛苦。尽管我们为了让整个团队都能使用而选择了Python,但还是有些人需要学习。而且我们的后端或全栈工程师都不懂Django(尽管前端的首席工程师很熟悉)。前端也类似,许多人都了解React,但很少有人有TypeScript的经验。尽管如此,在度过了最初的一段学习期之后,大家的效率都很快提高了,能够更快地一起学习。这就是第一个验证:即使对于新技术栈的经验很少,我们也能以远远超过以前的速度来构建功能。我们花了很长时间才确定,这些效率来自于新的技术栈和新的代码,而不仅仅是因为开了一个新项目。我们首先做的一件事情就是让所有人熟悉数据库。因为目标之一就是减少技术孤岛,让工程师在整个技术栈中感到舒适,因此我们带领不熟悉数据库的前端工程师思考数据库,并设计数据库的结构,然后与整个团队迭代。这样整个团队都会思考数据库。即使他们一段时间不处理数据库问题,也拥有了处理数据库的能力,并能提出有价值的问题。在那之后,我们以很快的节奏进行了几个月,重写了旧版本中我们喜爱的功能,并做了改进。我们在合理的时间内建立了一个非常好的项目。最初我们对于时间计划十分乐观,直到六月份都能按部就班地完成工作。此时,我们不断添加并修改功能,因为我们知道,没有这些修改,新版本不会成功。这降低了我们的速度,但根据我们的研究人员、客户团队和一些值得信赖的客户提供的信息,做这些是值得的。完成这一切后,我们有了一些非常引以为豪的成就,而且并不完全是技术成就:- 我们大幅扩展了团队。最初产品团队只有4名工程师,现在有9名,其中还不包括一个全职的QA/SDET团队,还有机器学习工程师团队,以及几名开发运维人员。在这个成长中,我们不仅避免了通常项目中增加人手带来的延误,甚至还提高了速度。(我认为主要是因为这是一个新项目。)
- 我们改变了工程在整个公司中的地位。尽管最初降低了一些发布新功能的速度,但人们看到我们能够非常迅速地重写已有功能,并看到新功能依然能迅速添加。有一次我们实时演示了Django Admin添加的一个功能,告诉大家我们现在的速度比以前更快。尽管是一个很小的演示,但非常有效。
- 我们从包含几个服务的面向服务的架构转变成了仅有一个服务的单一服务架构,而且可以开始设计容错性和横向扩展等。这在以前是一件非常痛苦的事情。
- 我们极大地提高了迭代的速度,很大一部分原因是因为新架构可以很好地适应我们的问题,现在每个人都可以舒适地做贡献。最好的一点是,机器学习团队偶尔也可以给真正的产品后台贡献代码了。
我们认为,成功的原因有以下几个。此外,我们从几个非常重大的错误中学到了一些教训。我们成功的原因之一就是,对于要构建什么拥有清晰的愿景(一个真正的MVP,我们知道旧的产品是“可行”的,因此我们需要做到同样的功能),而且我们按照需要缩减了范围,以保证愿景的清晰。尽管我们没能“按时”交付,实际上没人能恪守时间,但我们也没有像Netscape那样延误太久。整个项目的时间少于构建一个与旧产品功能上完全相同的产品所需时间的两倍,但我们得到的东西更好,而且得到了许多期待已久的功能,比如可以上传并发送视频,以及能够下载并自动生成PowerPoint报告等。另一个成功的原因是尽早获取反馈、尽可能经常地获取反馈。在重写过程中,我们非常频繁地在内部使用产品,以发现重大错误和性能问题。我们还经常为整个公司进行演示,从客户团队、销售、研究以及早期试用的客户那里获得反馈。那么,我们有哪些失误呢?我们决定采用两种以前没有怎么用过的技术。我们之前在一个原型中用过TypeScript,但并没有太多经验。当时用得不错,但还没有完全的把握它能提高生产率并降低出错的概率。时间会证明一切,我认为静态类型依然是正确的。另一个错误是采用了GraphQL。我们有很多REST和Redux的经验,但只在原型中使用过GraphQL。现在回想起来,有了GraphQL,最初的原型阶段很快,但需要付出长期的代价,因为Apollo中的一些关键性的决策我们并不能沟通(例如不给前端暴露检测连接断开或重新连接的接口),而且我们在后端调优方敏没有任何经验。我只能说,那一两个月是富有挑战的一两个月,我绝不想再尝试第二次。我们现在正在逐步去掉GraphQL,一方面是为了提高性能,另一方面让代码能够更好地承受慢速请求。关于重写,需要提及的最后一点就是,团队的士气会受到很大影响,你需要主动应对。刚开始一个新项目时大家的兴致都很高涨,但在构建了一些已有功能并修复一些bug之后,士气就会逐步滑落。从构建已有功能转到探索新功能,大家都很活跃,但重新构建已有功能就会很失落。尽管我们完成了重新构建,但一部分原因是我们在重新构建的同时也兼顾了探索新功能(毕竟这是重新构建的原因之一),而不仅仅是将旧的代码迁移到新的平台上。尽管如此,我们在这方面应该能平衡得更好。如果还有下次,我会着重确保让一些值得信赖的客户参与早期内部测试,并获得反馈和鼓励,让每个人都能对重新构建充满动力。我还会确保我们能在前期就开发一些新功能,而不是让旧功能拖得大家精疲力尽。一些无聊是不可避免的,但可以尽力减轻。
根据我的经验,如果你认为重写是错误的话,那么我认为你不应该重写。在任何情况下,你都应该默认选择“否”,然后尽最大努力判断重写是否真的有必要。- 如果你的架构或数据库结构严重不符合你的需求,而且由于架构不断更新或修改数据库结构非常困难等原因,没有清晰的迁移路线
- 如果当前技术栈非常限制团队的贡献,而且很难训练团队学习技术栈
即使满足以上所有条件,你也要考虑业务上的现状,以及重写是否适合你的团队和公司。除了上述几点之外,也许还要考虑其他因素。作判断很难,但我们需要三思而后行才能成功。https://remesh.blog/refactor-vs-rewrite-7b260e80277a【END】
更多精彩推荐
☞加码 2000 亿新基建还不够,阿里云再放话:今年招 5000 人!
☞议题曝光!百位顶级讲师、20大论坛,总有一个话题吸引你
☞张一鸣是如何练就字节跳动的
☞性能超越最新序列推荐模型,华为诺亚方舟提出记忆增强的图神经网络
☞DevOps 在移动应用程序开发中扮演什么角色?
☞稳定币经济:十大稳定币简史