← 返回博客

为什么大重写?

2024 年 1 月 23 日

上周是我在 Zed 的第一周。我加入时,团队正准备本周的发布,他们称之为“Zed 2”——预示着大事件即将发生。这次发布标志着团队多月重写 Zed UI 框架 GPUI 从版本 1 到版本 2 的结束。所有人都投入工作,修复最后的 bug,完善 Zed,而我,作为新来者,有太多问题:为什么要大重写?你们是怎么做到的?它带来了什么?你们是如何组织的?

幸运的是,Nathan、Max 和 Antonio——Zed 的三位联合创始人——很乐意坐下来回答我的问题。

以下是我们关于 GPUI 1 到 2 重写、其中存在的风险、为什么 GPUI 1 必须改变、团队如何围绕它进行组织以及代码之美为何重要的一个小时对话的编辑稿。我尽量保留了意图和含义,同时删除了那些“嗯”、“是啊”、半句话的提问、回溯和重复,这些都是反思性深度对话的组成部分。

(您可以在 我们的 YouTube 频道观看完整对话。)

Thorsten:即将发布的 Zed 版本是一个大重写。你们从 GPUI 1 切换到了 GPUI 2。我的问题是:为什么要大重写?听起来这是一项为期数月的大工程,我们都听说过不应该进行大重写。那么,你们为什么这么做?

Nathan:我同意。我听说过,我也知道。但我猜,我是一个非常喜欢重写的程序员。这是我工作流程中很重要的一部分。

当你与一个系统共存了足够长的时间,就很难摆脱根植于该系统中的基本 DNA:如果不是因为 B,我会修复 A;如果我已经修复了 A,我就会修复 B。事情变得几乎僵持。决策也受其他决策的阻碍。

首先,我职业生涯早期学到的核心教训是,人们常常低估了他们可以渐进实现的目标。我只想说这一点。大多数开发人员的原罪是未能意识到你可以通过渐进的方式达到目标。我当然也倾向于仍然犯这个错误,低估了可以通过渐进方式实现的目标。

但我也认为,在你通过构建 V1 真正深入了解一个问题之后,拥有巨大的力量,可以通过斩断所有那些相互僵持的限制和障碍,将系统彻底“液化”,然后重新阐明其核心思想,从而一次性将自己解放。

我认为我们承担了巨大的风险,我们可能通过增量方式在更短的时间内使 GPUI1 变得更好一些,但我不认为我们能达到现在这样好的水平。我们会在那些原本就不需要阻碍我们的事情上浪费太多时间。在我看来,这就是重写的优点。

Thorsten:是什么让你们开始这次重写?

Nathan:我慢慢地意识到了。它最初是对一个非常具体的问题的非常具体的解决方案,这个问题是 Derek,我们聘请的一位合同设计师。他进来时说:“我喜欢亲自动手。我喜欢构建 UI。”他曾与 Tailwind CSS 团队合作。他是一位非常优秀的动手型设计工匠,但他就是无法在旧框架中取得任何进展。

我们和 Nate 也遇到了同样的问题。他觉得自己可以在 Figma 中做一些事情,但之后不得不尝试让其他工程师来实现它。但我认为这确实造成了沟通瓶颈和奇怪的组织动态。

我突然意识到这是一个技术问题。这是事情的开始。很多时候,我认为工程师的一个失败模式是他们试图将技术解决方案应用于人的问题。但在这种情况下,我真的认为这是一个人的问题,如果有一个更有效的技术解决方案,它根本就不需要存在。

Nate 拥有与我不同的技能组合。我无法像他那样快速地设计出漂亮网站或 UI 界面,尽管我认为我还能应付。Nate 也能应付代码,他每天都在学习更多,但我们并没有让他感到轻松。

Antonio:对其他工程师也是如此,对吧?我记得我们中的任何一个人只要碰到 UI 代码,就会说:“天哪,我讨厌这个,为什么这么令人沮丧?”摩擦力太大了。整个团队都有这种感觉,不只是 Nate。

Nathan:我想,我们感到束手无策。

Thorsten:我只见过最新版本——GPUI2。它是一个受 Flexbox 启发的 DSL,带有 div.child,以及一个管理状态的模型和元素框架。GPUI1 是什么样子?它有什么令人痛苦的地方?

Max:它的灵感来源与 HTML 完全不同。它基于 Flutter 的布局模型。它对元素的布局方式有不同的、稍微受限的模型。在树中只有一个自上而下的布局过程,父节点将约束传递给子节点,然后子节点完全基于此决定它们的具体大小。它的功能严格弱于基于约束求解器的方式,即 Flexbox 的工作方式。

Nathan 想出了 Flutter 模型的这种改编,我们用了一段时间。一个问题是你必须采用那种约束向下、大小向上的心智模型。

但另一个问题是我们试图通过 Rust 编译时间使 UI 开发工作流变得可行的解决方案。我们有 Rust 代码来定义元素树,以及我们当时称之为主题的东西,这些东西不完全是主题,而是包含每个元素所有属性的巨大 JSON blob。

我们会在运行时加载它们,这样我们就可以做到这一点:我们有一个 TypeScript 文件,它会描述每个元素的每个属性。我们会在该层使用一定量的 TypeScript 抽象来减少重复,并对通用样式有一些概念。在这一层,我们会将其预编译为 JSON,然后应用程序会在运行时加载。我们认为这能让我们在运行时调整 UI 并让元素重新调整大小。

但结果是,它在定义元素的 Rust 代码和必须向每个元素应用样式的 TypeScript 代码之间造成了分裂。最终,在能够动态设置样式方面,它并没有像我们希望的那样奏效。它太复杂了。

理论上,我认为这听起来不错。我们认为我们可以做到,你可以拥有 UI 主题,并且某个开发者可以编写一个新的 JSON blob,完全重新设置应用程序中每个元素的样式,例如在 JSON 中定义类似 Zed 的 Material 主题,所有内容都将在运行时配置。

但它在实践中最终变得很糟糕。团队中的每个人都感受到了这一点。在运行时加载东西并没有让我们开发得更快。

所以我记得,在 Nathan 开始开发 GPUI2 之前,我的经验是,我们有点需要重新开始,重新审视我们在 Rust 中构建 UI 的工作流应该是什么。

Thorsten:是谁提议重写的?是有人说“我们来重写吧”,然后大家都说“走吧!听起来不错!”还是有人持怀疑态度?

Max:我想我们在 Zed 的开发达到了某个阶段,我们已经开发了一个系统,然后做了一段时间的工作,不涉及大量按钮、表单和 UI 等。它处于休眠状态很长一段时间。

Nate,那位设计师,习惯了这种情况,我们又雇佣了一些人,他们去做了不涉及新 UI 的工作。所以问题一直存在,但很长一段时间都不重要。

但突然间,我们想构建频道和不同的协作功能。然后每个人突然说:“嘿,我到底该怎么做……你们是怎么做到的?”答案是:“我们没有一个好的流程。”

这个问题有多种具体表现形式。而且,我认为,最初创建这个的 Nathan,他开始觉得:“每个人都在问我,你是怎么做到的?我还没有机会以我想要的方式解决它。所以我不知道。”

Nathan:说实话,我一个人并没有制造出主题的灾难。那是一个集体罪过。[大家笑]

但我负责布局系统。说真的,我有点照搬了 Flutter。多年前,Raph Levien 在他的一篇文章中给了我查看 Flutter 的想法。但我从未完全理解 Flutter 的思维方式。而且它也来自一种非常不同的语言,Dart。

我不知道,但我认为 Flutter 的很多工作方式都基于像 Dart 这样快速编译语言的假设。而我们更像是在网络场景中。总之,抱歉,我想我有点跑题了。我只是不想为我们主要的东西造成的混乱承担全部责任。

Antonio:它甚至不是以“哦,我们来重写吧”开始的,对吧,Nathan?它更像是:“我打算制作这个东西的原型,因为我对如何实现它有这个想法。我真的很喜欢 Tailwind 如何做 UI,并且对 UI 有这种不同的思考方式。”

我想它是从实验和原型开始的。我们有 GPUI 1,然后是 GPUI 2,某个时候甚至还有 GPUI 3。

最初的想法是循序渐进地开始,因为 GPUI 的很多功能不仅仅是 UI 部分,它还涉及到应用程序状态的管理。所以当时的设想是只取其中一小部分进行重新思考,只专注于我们如何渲染内容。

但随着事情的进展,我们之前谈到的摩擦,也就是“好吧,只想改变 UI,但这与框架的其他部分纠缠不清”的问题再次出现,所以这就是我们最终进行重写的原因。

但对我来说,重写的过程是非常渐进的。

Thorsten:据我所知,两三周前,当将代码库切换到 GPUI2时,你们基本上删除了 GPUI1 文件夹并重命名了 GPUI2 文件夹。这意味着你们同时拥有两个版本。你们是如何处理这个问题的?你们是构建了 GPUI2,然后通过功能标志来回切换,还是有其他流程?

Max:是的,我们有两个构建目标,它们是同一个东西的完整副本。在许多情况下,可能会有一个 crate 有两个版本,其中 90% 是相同的。一个针对 GPUI1 编译,另一个针对 GPUI2 编译。这个过程是,一旦我们开始将一个组件从一个版本迁移到另一个版本——例如:project_panel crate 变成了 project_panel2——我们就会停止在旧版本上编写新功能。它被冻结了,除非我们必须进行 bug 修复,这种情况下我们会同时在两个版本中应用修复。

Nathan:这种方法的理由是,它能让我们有机会继续共享代码。结果是,最便捷的过程就是克隆 crate 并放弃旧代码。

但如果事情比我们预期的要花更长时间,或者出于任何原因,我们基本上有两个可以自由混用代码的并行系统。如果需要,我们还可以引入第三个共享大量共同点的 crate。我认为这保留了很多选择性,而我们在进行像这次重写这样高风险的举动时,感觉这是我们需要的。

在早期,也有增量性,这一点值得讨论。GPUI2 crate 有一段时间是基于 GPUI1,并导入了几乎所有 GPUI1 的内容。我们基本上在 GPUI1 的基础上运行了一个 GPUI2 的原型。GPUI1 也有其定义元素的方式,我们实际上让一个 GPUI2 元素成为一个有效的 GPUI1 元素。当然有很多漏洞,但它至少让 Nate 能够开始使用新框架,并迅速验证这确实是一个值得去的地方。

Thorsten:这次重写的时间线是怎样的?你们是什么时候开始的?去年年中,然后三周前才合并,对吧?我记得我们第一次谈话时,你们都在意大利,这项工作已经在进行中,对吧?

Nathan:是的,那是在十月下旬。在十月最后一周我们意大利相聚时,整个团队才开始参与其中。

但在那之前的一周我与 Antonio 合作,在那之前的一周我拼命工作,尽可能地推进事情,这样当 Antonio 和我在一起并有专注时间时,我真的想达到一个……我的意思是,我不知道,我当时以为框架已经接近完成,但实际上根本没有。Antonio 和我,我们那周也工作了很长时间。目标是让这个东西准备好,以便整个团队都能在此基础上进行构建。而且,你知道,我们做到了。

但我认为我们可以承担更少的任务。视图方面的问题是急需解决的突出问题,即 UI 元素和布局等所有这些东西的工作方式。

然后还有其他问题。最初的 GPUI 1 设计之初是为了向 Electron 应用程序吐出 JSON。Zed 最最初的 UI 是一个 Electron 应用程序,它启动了一个 Rust 二进制文件。我们尝试了几种不同的方法。其中一种是只运行一个子进程。然后,更早的时候,我们实际上是将 Rust 作为一个库嵌入,通过 Node,通过 V8 嵌入 API 进行通信。你还记得吗?那是很多年前的事情了。

Thorsten:这是什么时候的事?这是在 Zed 公司成立之前,对吗?

Nathan:是的,那是在 xray 时代。那是 2018 年。

Thorsten:哇。

Nathan:是的,我们有很多假设,比如我们向这个 Electron 层吐出 JSON。

由于这种最初的设计限制,模型层奇怪地意识到了表示层。你正在管理另一个进程的状态,这些 JSON 池需要进出作用域等等。它是为此设计的,然后又进行了调整。

而且所有权的方向性会变得非常奇怪。所以我想彻底改变,说现在所有的箭头都指向同一个方向。不要让模型代码了解视图代码。它没有那么糟糕,但这是另一个感觉非常重要的修复。

另一件事:老实说,这是我真正编写的第一批 Rust 代码。我们早些时候用 CRDTs 写了一些 Rust 代码,但在那个领域,这是一个非常不同的问题。我不知怎么开始痴迷于为所有东西赋予类型,但我们使用的所有 GPUI 1 代码到处都是 usize,而我们本应有专用的类型。我们有 Pathfinder 的几何类型,因为 GPUI 经历了一个用 Pathfinder 渲染所有 2D 图形的时代,所以我们有所有这些奇怪的向量类型。我们没有在各处传递的基本类型。我们没有类型系统中的像素概念,所有都是 f32 等。我们可以忍受它,但当我们这样做的时候——这有点像你让自己购买最昂贵的变体的方式:“好吧,既然我正在做那个,我也可以做这个。”它就是这样悄悄地发生了。是的。

Antonio:我们一直说这有风险,但我认为这从来不是说我们做的事情没有意义。我们不担心我们做的事情没有意义,或者我们做不到。这更多的是组织风险。我们有这另一个代码分支,我们如何适应它并让整个团队致力于过渡?

Nathan:风险还在于:这需要多长时间?会有多少 bug 出现?我什至不知道我们现在是否知道答案。

Thorsten:你们估计这需要多长时间?

Nathan:最初的估计是我们在意大利的团队峰会结束时,也就是十月底之前完成。那是我们希望的,这太疯狂了,对吧?所以最终,我猜,比最初估计多花了两个月。但是当我们接近那个估计时,那还相当早,我们重新调整并将估计设定在年底。我们确实达到了那个估计。

我知道如果再拖延下去,我们就不应该做了。是的,我就是这么想的。

Thorsten:Antonio,你说这不是一个能否做到的问题,更多的是需要多长时间以及其他因素。

Antonio:是的。你有一个团队的人在处理这个代码库,每个人都必须互相协调。我们还必须告诉每个人如何迁移:克隆这个,创建这个,使用这个版本——你如何协调这个过程?

我觉得那次峰会(在意大利的团队会议)对此真的很有帮助。而使用 Zed 进行协作的能力是峰会期间高效工作的关键。我们有三四人一组在同一个工作副本上,修改和复制不同的文件。在时间上也非常好,因为这是一个很好的时机来分享这个新框架的假设、大体想法的背景信息,并达成共识。峰会之后,我觉得,我不知道,我认为事情进展得相当顺利。

Nathan:我觉得 Zed 几乎开启了一种完全不同的工作方式。Zed 加上 Rust。你实际上可以让四个人在一个工作副本中,消除数百甚至数千个编译错误。

实时地在一起,做大量的原始工作,修复所有这些错误等等,但所有这些都可以并行完成。所以发生了一种超线程,而不是典型的过程。

Thorsten:真有趣。这就是 Antonio 和我这周所做的事情。当我们不得不进行大量的重命名时,他对我说:我从底部开始,你从顶部开始。我觉得这真的很棒。

Nathan:是的,直到吃了意大利面才觉得没那么好。我只是不停地吃意大利面。意大利面真的解决了痛苦。但是的,这感觉很有活力,每个人都了解了新框架。这为我们之后分开独立工作奠定了基础。

Thorsten:回过头来看,你们三位,会说这是一次成功吗?

Antonio:我会说百分之百。是的,那很紧张,我不知道这对我的健康,或任何人的健康来说是否成功。我不知道我们是否应该提到这个。但我对代码的现状很满意。所以我会说是的。

Max:我也很满意代码的现状。有一些非常紧张的时刻——有点像往团队和代码库里扔了一颗炸弹。但酷的是,它给了每个人一次塑造性的经历,让他们融合在一起。他们说,他们亲手重建了许多我们很久没有动过的东西。

我想,很明显,如果你拥有完美的知识,你或许可以规划一条更有效率的路线来解决我们打算解决的问题。但你也可能在很多方面变得更糟。你可能什么也没做。我们可能仍然会继续迭代一个比它本应更难的东西。我们可能,就像 Nathan 所说,效率低下地,逐渐地,一次一个,一个 PR 接一个 PR,一步步走到这里。那也会有挑战。

我认为这百分之百是成功的,因为风险已经过去了。我们会稳定它。现在它已经工作了。我们解决了我们着手解决的问题,那就是视图很难构建,而现在不再是了。这付出了很多心血。但我们一开始没有人能说,这里有一种更好的方法可以在更短的时间内解决这个问题。

Nathan:是的,成功了。

Max:我记得在重写开始时,我与团队中不同的人进行了一对一的交流。当时我们告诉大家,“嘿,构建 UI 有点困难,我们需要做一些改变。Nathan 正在开发一种全新的元素系统。”当时,我以为事情会是这样:我们将改变 GPUI 中元素的工作方式。即使那样,在当时也感觉是一项非常大的工程。

所以在我们一对一的交流中,人们会问,如果只是改变所有元素的工作方式,那会怎么进行。但我与 Nathan 和 Antonio 合作了足够长的时间,也看到了它是如何运作的,同时又有人告诉我们“你们应该更渐进地进行”。这在过去也发生过。我记得在 Atom 上,Nathan 有一个重写文本编辑器工作方式的愿景,从概念 A 到概念 B,我只能对其他人说——作为一名工程经理,这有点难以启齿——这不是我们通常做事情的方式,但我绝不会押注 Nathan 拥有理想工作方式的创造性愿景,然后 Antonio 来到并无论什么坏了都能让它工作。我绝不会押注这会失败。

所以我只能说,是的,这不典型,但相信我,就像我信任他们一样。

Nathan:谢谢你们为我们担保。

Antonio:说得真好,Max。

Nathan:但有那么一刻你就像,“伙计们,这他妈太糟糕了。”[笑声]但我对你们俩愿意做这些疯狂的事情感激不尽。这可能是不明智的,因为,我不知道,这些年我也犯过错。

Thorsten:你们解决了视图难以处理的问题,重写之后还有哪些事情变得更容易了?

Nathan:它比写博客要容易得多。这是一件事,很重要,因为这确实是整个系统的基础。

如果有人要为 Zed 做出贡献,他们需要理解系统中的思维方式。还有什么比在源代码中清晰地呈现我们解释的思维方式更好的理解方式呢?

旧系统就像是通过一层朦胧的滤镜审视底层概念,夹杂着许多噪音,与我脑海中关于事物应如何运作的核心思想混杂在一起。

现在表达得很清楚,虽然不完美,没有什么完美,但清楚多了。所以这是一件事。它会更容易学习,更容易教学,我想也更容易上手。我们还需要移植到另外两个平台。所以我想这很重要。

Thorsten:Nathan,你之前提过,目标一直是让 Zed 开源,你也说过你希望在开源之前完成重写。那么,这是动机的一部分吗?我们必须先让代码库达到这种状态,然后才能开源?

Nathan:我想 Max 提到这就像在团队中间投下一颗炸弹。你知道,你可以在一个房间里的小团队中间投下一颗炸弹。

Thorsten:然后你击中了他们所有人,是的。

Nathan:没错。哦,天哪。是的,这有点黑暗。[笑]

Thorsten:是的,也许吧。

Max:新比喻。

Nathan:你可以给一个房间里的小团队提供迷幻剂,但不能给整个开源社区。

所以这是另一件事。这里有一个机会可以获得很多好处,并把事情整理好,而开源之后再做这些会非常困难。我认为,引入太多的混乱。

我不确定我们是否还能再做类似的事情。希望我们永远不需要。

Thorsten:Max、Antonio,你们想补充点什么吗?你们认为在开源之前清理代码库很重要吗?有很多成功且伟大的项目,即使维护者也会说它不是一个完美的代码库。

Antonio:我认为这很重要。例如,我们开始做的是文档化 GPUI 中的函数。我很难想象在旧代码库中做这件事。现在我觉得团队中的任何人都可以查看 GPUI 中的代码并进行文档化。我认为这对于开源很重要。

另一件事是,为了我们作为一个公司想要实现的目标,我认为我们需要成为一个非常好的开源项目,我们需要得到所有能从人们那里得到的帮助。因为我们是一个相当雄心勃勃的项目,我不认为我们能独自完成。我们需要每个人都参与到这项工作中来,所以我真的认为它很有价值。

Max:我想你说的有道理,开源代码不代表代码必须完美。我的意思是,现在完美了吗?我之前就会开源它并支持它,如果它有一些奇怪的地方,我也会说,是的,现在就是这样。我认为这很好。你不必在开源之前让你的代码库与你想象中的理想状态保持一致,但这本来就是我们想做的事情。而且我认为它具有效率:如果我们很快要开源,并且很快也要重写来解决这个问题,那么就先重写,然后开源,而不是反过来。

Nathan:开源需要精力、努力和参与。

我只想说另外一件事,我就是个彻头彻尾的完美主义者。我是说,我确实花时间为此道歉,而且我不会说“完美”对我来说很重要,但“美”对我来说很重要。

我们为此奋斗了很长时间。我工作得太努力了,不能不关心美。我们将代码发布出去,所以现在它是我在这个世界中表达的一部分。如果我为了钱,我会在 2014 年左右开始一个加密货币什么的。做这件事的全部原因,真的,比任何事都更重要的是自我表达。而代码就是这种表达的一部分。

现在,我认为存在一种失败模式,即人们沉迷于代码本身,他们所做的只是欣赏他们有多少个单子或诸如此类。

但代码要做到优美和精巧,我认为这是有价值的。只要它不变成一个渺小的神。


正在寻找更好的编辑器吗?

您今天就可以在 macOS、Windows 或 Linux 上试用 Zed。立即下载


我们正在招聘!

如果您对我们博客中涵盖的主题充满热情,请考虑加入我们的团队,帮助我们实现软件开发的未来。