重构与重写

2020-06-03 01:50:00

乔尔·斯波尔斯基(Joel Spolsky)在一篇著名的博客文章中断言,你永远不应该从头开始重写代码库。他举了网景的例子,他们花了几年时间重写软件,他们的公司最终在这个过程中倒闭了。一年多前,我重读了那篇帖子,但我仍然投票赞成从头开始重写我们的申请。全部都是。这就是我们这么做的原因,我们是如何成功的,以及一些关于你是否应该这样做的启发式方法。

我们的故事从2019年1月开始。Remesh当时是一家小得多的公司。我们最近雇佣了几名工程师,我们有5名工程师专注于产品,还有几名工程师专注于机器学习(ML)或DevOps。尽管最近雇佣了这些工程师,但我们的速度仍然低得令人痛苦。添加简单功能需要很长时间。我们在产品中有很多错误,我们只是简单地承认这些错误是“已知的”,并没有修复。而且整个产品看起来已经有一段时间没有明显变化了。

理解我们为什么会有这些问题是很重要的。我们假设(在重写之后,验证了)问题不在我们的员工身上:我们雇佣了伟大的工程师。问题主要出在我们的代码库和我们的流程上。我们正在工作的遗留代码库既不适合我们团队的技能集,也不适合我们正在解决的问题,我们的过程鼓励并依赖于孤立的知识:在Remesh没有“全栈”。

我们的遗留应用程序最初是为一些与现在使用的应用程序非常不同的东西而设计的。最初,Remesh允许用户在整个组之间或个人和组之间进行双向对话。例如,你可以让民主党人和共和党人相互交谈,相互理解,找到共同点。或者,你可以让镇长与他们的市民交谈,以更好地了解他们需要什么、相信什么和想要什么。然而,当我们发现产品适合市场时,用例发生了变化。我们靠向一位单独的主持人,与一群人交谈。

这种更改的结果是,某些旧的设计决策不再有意义,模式需要进行重大更改。除了数据库问题之外,我们的代码库本身也很难理解,因为特性都是用螺栓固定起来的,没有进行太多的重大重构。我们在最需要重构的领域的测试覆盖率非常低,因为它们是最旧的代码,在我们建立良好的测试实践之前编写。

除此之外,正在使用的语言和框架对我们的团队不起作用。后端代码库是用Elixir编写的,我们的开发人员中很少有人非常了解这一点。其中一个前端代码库是用非常古老的ANGLING版本编写的(我不想去检查它是什么,我可能会哭),我们还有另外两个前端在响应。我们的工程师中很少有人在一辆车里感到舒服,更不用说三辆车了。使用的语言和框架不适合我们的团队,也不适合我们的问题,这大大降低了我们的速度。

在这一点上,我们知道我们的代码库需要进行重大更改。当您面前有一堆难以处理的代码时,确实有三种选择:

对于前端来说,重构并不是一个真正的选择。我们的ANGLE版本已经足够老了,不幸的是,我们并没有明确的升级到现代版本的ANGLE(坦率地说,我们也不想使用任何版本的ANGLING)。因为我们预计UI和API会有重大变化,所以重构是不可行的。因此,在前端,我们要么一举重写它,要么零敲碎打地重写它。

后端有一些我们想要解决的问题--我们的模式、语言和代码库不再适合我们正在解决的问题。我们将Elixir用于其大规模的并发支持,但我们最终从未需要它,它只会咬我们一口:Erlang VM中处理并发的方式使得分析非常困难,因为您知道正在计算什么,但不知道从哪里调用它-祝性能调优好运。Elixir代码库还限制了我们的ML工程师对后端代码库的贡献:他们每天都在Python中工作,没有时间深入研究Elixir。长话短说,我们想放弃ELEXIR,转而使用Python,因为这样我们的整个团队就可以做出贡献,语言将支持问题,我们将更容易分析代码。

我们也有一些“产品债务”,我们向现有客户介绍了他们学习并逐渐依恋的概念,但最终可能并不理想。他们是当地的极端分子。如果我们想要打破局部最大值,做出更好的东西,我们可能不得不进行一次大的飞跃,在那里,较小的迭代可能会遇到用户的抵制。删除中的这些概念需要一次做很多事情。

归根结底,我们重写的理由实际上可以归结为以下几个因素:

我们希望我们团队的每个成员都能为后端代码库做出贡献,而Python既易于学习,又在我们的团队中得到了广泛的采用,这符合我们的要求。

我们的旧代码库非常脆弱,测试难度很低,重构它将是一个艰巨的过程。

我们可以通过迁移到像Django这样自以为是的框架来提高效率,同时也有很多省时的默认设置(比如Django Admin)。

我们将有机会制作一个受我们从客户那里学到的东西影响的全新版本,然后就可以管理向新版本的过渡,而不是在每一项更改上都与大量客户进行长达12个月的斗争。这也使得培训我们的客户成功团队和销售团队最终成为一次性的批量工作,而不是不断地重新引入新概念。

为了做出这个决定,我们做了相当广泛的规划。我们可以整天谈论敏捷这个和那个,但是这是一个实际执行瀑布式计划的案例-不是因为我们要实现它的瀑布式,而是要计算出每个选项的努力程度。很快就很明显,批量重写应用程序需要时间,但是重构或零散重写需要更长的时间,而且围绕它的不确定性要高得多。如果我们走重构路线,我们将冒更大的风险。

最后,我们对自己的决定很有信心,而且我们得到了组织各个层面的支持。我们决定重写,因为它可以让我们修复过去几年的错误,同时也可以让我们明显地推动产品向前发展。

我们在2019年2月开始重写,此前我们规划了需要包括哪些功能才能与现有平台的功能相当,这是我们尽职调查工作的一部分,以确保这是正确的道路。因此,我们有一个非常坚实的计划,特别是围绕我们正在建设的东西。它违背了敏捷(或者是敏捷?🤔)的教条,但是拥有一个我们愿意背离的计划有助于指引我们前进的道路,看看我们是否走上了不同的道路。当我们对用户(内部客户,最后是一些外部客户)进行测试时,我们确实出现了相当大的偏差,但稍后会详细介绍这一点。

在经历了一个坎坷的开始之后,构建新版本的实际过程相当顺利。对于每个人来说,切换到新的技术堆栈都是痛苦的。虽然我们选择Python作为整个团队的可访问性,但我们中仍有一些人需要学习它。而且我们的后端或全栈工程师一开始都不认识Django(而我们的首席前端工程师对它非常熟悉)。同样,在前端,我们中的许多人都知道Reaction,但很少有人对Tyescript有深刻的经验,我们也选择迁移到Tyescript上(这里有一些故事可以在下一次讲述)。也就是说,在我们有了一些最初的学习时间之后,我们很快就变得相当有成效,我们能够一起学习。这是我们的第一个验证:即使在这个新的堆栈中经验较少,我们也能够更快地构建特性。要确定生产率的提高来自新的堆栈和新的代码库需要更长的时间,而不是简单的绿地项目,但我们最终还是做到了。

我们做的第一件事之一就是让每个人都接触到数据库端。由于我们的目标之一是减少孤岛并增加整个堆栈中工程师的舒适度,因此我们指导一些没有数据库设计经验的前端开发人员在早期思考和设计我们需要的对象的初始模式的过程,然后我们与整个团队进行迭代。这使他们能够考虑数据库方面的问题。尽管他们已经有一段时间没有这样做了,但他们有能力这样做,并且可以提出真正具有挑战性的问题。

在那之后,它几乎以我们几个月来最快的可持续速度运行,重写了我们在旧版本中知道和喜欢的东西,并在我们让它们更易用的同时对其进行了调整。我们在合理的时间线上建造了一个非常好的项目。我们一开始的日程安排非常乐观,一直到6月份左右我们都在按部就班地进行。在这一点上,我们添加和更改了功能,因为我们知道没有它们,新版本就不会成功。它拖慢了我们的速度,但基于我们内部研究人员、客户成功团队和一些值得信赖的客户的真实反馈,这是必要的。

在整个过程中,我们实现了一些我最引以为豪的事情,这些并不都是技术上的成就:

我们戏剧性地壮大了团队。我们最初有4名产品团队工程师,现在是9名,这还不包括雇佣一个完整的QA/SDET团队,为我们的ML工程团队增加一些人员,以及雇佣DevOps工程师。在这种戏剧性的增长期间,我们不仅避免了通常的项目延误,因为增加了人员-不,我们加快了速度。(我认为这在很大程度上是因为这是一个绿地项目。)。

我们改善了整个公司对工程的看法。在发布新功能的速度稍微慢了一段时间之后,他们发现我们至少可以快速重写现有的东西,并看到新功能也在快速添加。有一次我们做了一个很酷的演示,对Django的Admin进行了现场编码,演示了是的,我们现在可以做的很多事情都比以前快得多。这是一个小小的演示厅,但很有效。

我们从只有几个服务的面向服务的体系结构转变为只依赖一个服务的整体,并且我们从一开始就开始设计容错和水平可伸缩性。这在以前是一个很大的痛点。

我们极大地提高了迭代的速度,这在很大程度上是因为我们有一个新的架构,它适合我们的问题,并且在堆栈中,每个人(现在)都很乐意为之做出贡献。最棒的是,ML团队现在可以而且确实偶尔会为我们的实际生产后端做出贡献。

我们相信我们成功的原因有几个。我们还了解到(回过头来看)我们在这个过程中犯了一些相当大的错误。

我们之所以成功,是因为我们一开始就对我们正在构建的东西有一个清晰的愿景(一个真正的MVP,我们知道旧产品是“可行的”,所以我们必须做到这一点或更少),我们根据需要缩小范围,以便保持清晰的目标。虽然我们没有“按时”发货,但没有人发货,我们也没有用网景的长度。项目的总持续时间不到我们预计的两倍,如果我们构建一个旧产品的完全相同的副本(就功能而言),但我们最终得到了一些更好的东西,并且具有一些新的非常需要的功能,比如上传和发送视频的能力,以及下载自动生成的对话PowerPoint报告的能力。

我们成功的另一个关键之一是及早并经常获得反馈。在重写过程中,我们经常在内部使用该产品,发现了严重的错误和性能问题。我们还定期为整个公司举行演示,以便从客户成功、销售、研究以及最终从可以容忍失败的早期采用者客户那里快速获得反馈。

那么,我们哪里出了问题呢?嗯,我们决定采用两种我们以前不常使用的技术。我们以前在原型中使用过打字稿,但我们在这方面没有很深的专业知识。它进行得不错,但我们仍然不相信生产率更高,缺陷率更低;时间会证明一切,我认为静态打字还没有定论(如果有人对此有明确的研究,我希望您能把它们寄给我)。另一个错误是使用GraphQL。我们对REST和Redux有相当高的经验,但我们以前只在原型中使用过GraphQL。回过头来看,GraphQL使最初的原型开发速度更快,但要付出长期的代价,因为在Apollo中有一些关键的设计决策是我们不同意的(比如不公开在前端的订阅中检测断开/重新连接的能力),并且我们在后端对其进行性能调优的经验是…。这么说吧,这是我生命中极具挑战性的一两个月,我再也回不来了。我们现在正处于摆脱GraphQL的过程中,对于性能关键型的应用,快速迁移,而对于更能容忍慢请求的应用,随着时间的推移,我们将缓慢地迁移GraphQL。

最后要注意的是,在重写时,您的团队会受到影响,士气也会受到影响,您必须积极应对这一点。一开始开始一个新项目是相当令人兴奋的,但接下来的工作是构建我们已经拥有的功能和修复错误。过一段时间它就会变薄了。看到我的团队在从构建我们必须构建的东西转移到探索新功能时有多么活跃,这是相当令人惊讶的,这让我意识到重建可能真的会让人精疲力竭。我们成功地完成了重建,部分原因是我们平衡了对新功能的一些探索(毕竟,我们重建这个功能是有原因的),而只是将我们的旧代码直接移植到新平台。也就是说,我们绝对可以更好地平衡这一点。下一次,我将重点确保我们有一个早期的阿尔法测试计划,有几个值得信赖的客户,以获得定期的反馈和鼓励,并保持每个人对重建的兴奋。我也会确保我们在早期加入了大量的新功能,而不是一旦我们发现每个人都有点累了就开始工作。有些单调是不可避免的,但你可以减轻它。

根据我在这里的经验,你可能是…。不应该,如果你相信改写从来都不是正确的决定的炒作。无论如何,你应该默认“不”的立场,然后在必要的时候非常非常努力地证明它是合理的。

如果您的体系结构或模式与您的需要严重脱节,并且没有明确的迁移路径,因为增量更新体系结构或模式将非常困难。

如果您当前的技术堆栈限制了许多工程师的贡献,并且在技术堆栈中对他们进行培训不是一种选择。

即使你做到了所有这些,你也必须考虑商业现实,以及这对你的公司、你的团队是否有意义。

可能有更多的场景可以证明重写是合理的。证明它是正确的是困难的,但它是值得的,它可以成功地实现。