标签:除了 快速 moc 基于 例子 编写 架构 演示 支持我
重构,还是重写?(2020版)Joel Spolsky (软件随想录作者)曾经写过一篇著名的文章, Things You Should Never Do (1) ,他在文章中断言,你永远不应该从头开始重写一个代码库。他举了 Netscape 公司的例子,他们花了好几年的时间重写软件,最终公司在这个过程中死亡。一年前,我重读了那篇文章,但还是选择了从头开始重写我们的应用,对,全部重写。以下介绍为什么这么做,我们是如何成功的,以及一些关于你是否也应该这么做的启发式分析。
故事要从 2019 年 1 月说起。当时,Remesh 还是一家比现在规模小得多的公司。当时招聘了一些工程师,有 5 名工程师专注于产品开发,还有一小部分工程师负责机器学习(ML)或 DevOps。尽管有这些工程师,但开发速度非常缓慢,简单的功能需要很长时间才能完成,产品有很多已知的 bug 没有修复,而且整个产品看起来很长时间没有明显变化。
了解为什么会有这些问题是很重要。假设问题不在人上面,我们有优秀的工程师(之后新版的成功也验证了这一点)。问题主要出在代码库和流程上。我们所使用的历史代码库,与团队的技能和业务场景并不匹配,当时的流程也鼓励和依赖工程师垂直领域的知识,也没有 "全栈"工程师。
旧版应用的设计初衷与现在的版本截然不同。最初,Remesh 让用户在整个群之间或者一个人和一个群之间进行双向对话。例如,你可以让 Democrats 和 Republicans 各自对话,互相了解对方,寻找共同点。或者,你可以让一个城镇的市长和他们的市民对话,以更好地了解他们需要什么、相信什么、想要什么。然而,当我们找到了产品与市场的契合度后,用例也发生了变化。我们倾向于由一个单一的主持人与一群人交谈。
需求变化的结果是,某些旧的设计方案不再有意义,schema 需要进行重大改变。除了数据库之外,代码库本身也很难理解,因为这些功能都是在没来得及进行大的重构的情况下,被开发人员用螺栓连接起来的。在最需要重构的地方,测试覆盖率很差,因为这些代码是最老的代码,是在建立良好的测试实践之前编写的。
除此之外,语言和框架也不适合我们的团队。后端代码库是用一种叫 Elixir 的语言开发,而开发人员很少有人熟悉 Elixir。其中一个前台代码库是用非常老旧版 Angular(我甚至不想去了解到底是哪个版本,往事不堪回首),我们还有两个前台是用 React 写的。但工程师几乎没人了解其中一项技术,更不用说这三个都会。使用的语言和框架并不适合团队和我们的场景,这让开发速度非常慢。
毋庸置疑,我们的代码库需要一个重大的改变。当你面前摆着一堆代码,很难往前推进时,大概有三个选择:
对于前端,重构并不是一个合适的选择,Angular 版本已经太老旧,以至于没有任何明确的升级路径可以升级到现代版本的 Angular(老实说,任何版本的 Angular 都兴趣不大)。而且由于预计 UI 和 API 会有重大变化,所以重构是不可行的。因此,在前端,我们只能选择一次性重写,或者逐步小范围重写。
后端有一些需要解决的问题 — 当前的模式、语言和代码库都不适合我们的场景。我们使用了 Elixir,因为它有强大的并发支持,但我们最终不太需要这个功能,而且它反而陷住了我们:Erlang 虚拟机中处理并发的方式使得代码分析变得非常困难,你知道计算的是什么,但不知道从哪里调过来的 — 祝你在性能调整方面好运。
Elixir 的代码库也限制了机器学习工程师对后端代码库的贡献:他们每天都在 Python 中工作,没有时间深入学习 Elixir。长话短说,我们想放弃 Elixir,转而使用 Python 语言,因为这样一来,整个团队就可以参与贡献后端代码,这门语言可以解决我们的需求,而且分析代码更加方便。
我们也有一些 "产品债务",老版本向用户引入了一些新东西,他们接触之后也逐渐喜欢上了这些理念,但最终效果并不理想。它们是局部的极端。如果我们要跳出这个局部极限,做出更好的东西。我们必须要做一次大的改版,在这个过程中,较小的迭代可能会不断遇到用户的阻力。去掉之前这些功能,需要同时做很多事情。
归根结底,重写的理由其实归结为以下几个因素:
为了达成这个决定,我们做了相当广泛的规划。虽然整天谈论敏捷和精益什么的,但这次实际是一个瀑布式的开发 — 不是因为我们要实施瀑布式的计划,而是我们发现重写应用程序需要不少时间,但重构或零散地重写需要更长的时间,而且不确定性要高得多。如果走重构路线的话,我们要冒的风险会更大。
最后,我们对自己的决定很有信心,而且公司的各个层面都支持我们。我们决定重写,在让产品向前发展的同时,修复过去几年来的错误。
让重写开始吧。
我们在 2019 年 2 月开始重写,在规划出功能范围之后,就开始启动重写,作为尽职工作的一部分,我们围绕着我们要开发的功能,制定了一个非常坚实的计划。这违背了敏捷的教条,但有了一个可以调整的计划,有助于指导我们前进的道路,看看是否偏离了轨道。当我们与用户(内部用户和一些外部客户)在进行测试的阶段,我们最终确实偏离了不少计划,更多的内容会在后面说。
在经历了一开始的坎坷之后,构建新版本的实际过程还算顺利。对于工程师来说,切换到一个新的技术栈是痛苦的。虽然我们选择了 Python 来达到最低的切入成本,但仍然有一些人需要学习。而且我们的后端工程师也没接触过 Django(但我们的首席前端工程师对 Django 有很深的了解)。同样,在前端方面,很多人都知道 React,但很少有人对 TypeScript 有深入的经验,我们选择 TypeScript 语言(这有一些故事要留待后文会说)。有了一些初步的学习时间后,我们都很快就有了相当大的收获。
这是我们第一个验证得到的经验:即使在这个新的技术栈中经验较少,也能更快地构建功能。要确定生产力的提高是来自于新的技术栈和新的代码库,而不是仅仅是一个空项目,这需要更长的时间,但我们最终还是达到了目标。
首先做的一件事就是让大家接触数据库。由于我们的目标之一是减少信息孤岛,让工程师尽可能了解整个技术栈,所以我们引导一些对数据库设计没有什么经验的前台开发人员,让他们去思考和设计最初的数据访问版本,然后和整个团队一起迭代。这使他们有能力去参与数据库方面的问题。尽管他们已经很久没有参与这方面工作,但仍然表现出了这方面能力,并能提出一些真正具有挑战性的问题。
在这之后,我们快速前行持续了几个月,重写了旧版本中熟悉的和感兴趣的东西,并在不断的优化,使其更加好用。我们在合理的时间内完成了一个非常好的项目。一开始,时间表非常乐观,直到 6 月左右,我们一直在按计划进行。不过后来增加和改变了一些功能,因为我们知道没有这些功能,新版本就不会成功。这让项目速度慢了下来,但来自内部研究人员、客服团队和一些值得信赖的用户的真实反馈,对我们项目成功是必要的。
在整个过程中,我们取得了一些我引以为傲的成绩,不全是技术方面的。
我们相信我们是成功的,当然过程中也犯了一些相当大的错误。
之所以成功,是因为我们一开始就对我们要打造的东西有一个清晰的愿景(一个真正的 MVP,我们知道旧产品是 "可行的",所以我们必须达到这个目标或更少),我们根据需要削减范围,以保持清晰的目标。虽然我们没有 "按时交付",但也没有变成 Netscape 的方式。项目总工期不到预计的两倍(基于完全复制旧产品功能的预期时间),但我们最终得到了一个更好的产品,并且有一些新的功能,比如上传和发送视频的能力,以及下载自动生成的 PowerPoint 报告等。
成功的另一个关键是尽早并经常获得反馈。在重写过程中,我们经常在内部使用产品,发现关键的 bug 和性能问题。我们还定期举行全公司的演示会,从帮助客户成功、销售、研究,以及从能够容忍各种问题的早期试用用户那里快速获得反馈。
做错的事情有哪些?我们曾经引入两个我们以前不怎么熟悉的技术。我们之前在一个原型中使用过 TypeScript,但我们对它没有很深的专业知识。进展虽然马马虎虎,但我们仍然不相信生产率会更高,缺陷率会更低;时间会证明,静态类型的语言会更佳(如果有人对此有确切的研究,我很乐意你把它们发给我)。
另一个失误是使用 GraphQL。我们在 REST 和 Redux 方面有相当高的经验,但之前只在一个原型中使用过 GraphQL。现在回想起来,GraphQL 让最初的原型开发速度快了很多,但长期的代价是,Apollo 中有些关键的设计决策我们并不认同(比如没有在前端暴露出检测订阅中断开/重连的能力),而且在其后端的性能调优经历也是一言难尽……那是我人生中非常艰难的一两个月,我再也不想回去了。我们现在正在从 GraphQL 中迁移出来,对于性能关键的东西,会快速地进行迁移,然后再慢慢迁移那些对请求性能容忍度较高的调用。
最后需要注意的是,在重写的时候,你的团队以及士气会受到影响,你必须要积极应对。一开始启动一个新项目是相当令人兴奋的,但接下来的事情就是构建已有的功能和修复 bug,过了一段时间就会觉得很累。很欣慰看到我的团队从构建我们已有的功能到开发新的功能,我也意识到重写工作真的很耗费精力。
我们成功地完成了重建,其中的一部分原因是平衡了新功能开发与旧代码迁移。话虽如此,我希望我们在平衡方面能做得更好。下一次,我将集中精力确保我们有一个早期的 alpha 测试计划,与几个值得信赖的用户一起进行测试,以获得定期的反馈和鼓励,并让大家对重建保持兴奋。我还会确保我们在早期就加入大量的新功能,而不是发现大家都有点疲惫,才开始引入新功能。有些单调是不可避免的,但你可以减轻它。
根据我的经验,你也许不应该像我这么做,如果你深信重写永远不会是正确的决定那些文章。无论如何,你应该默认为 "不重写" 的立场,然后非常努力地推进,并证明不重写是正确的。
但有几种情况,重写可能是合理的。
即使所有这些都符合你的情况,你也要进一步考虑企业的实际情况,考虑到这对你的公司、你的团队是否有意义。
有可能在更多的情况下,重写是有道理的。辩解这一点很难,但它可能是值得走的一条路,而且可以成功地完成。
https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i/
英文原文:
https://remesh.blog/refactor-vs-rewrite-7b260e80277a
参考阅读:
本文由高可用架构翻译,技术原创及架构实践文章,欢迎通过公众号菜单「联系我们」进行投稿。
标签:除了 快速 moc 基于 例子 编写 架构 演示 支持我
原文地址:https://blog.51cto.com/14977574/2546122