← 返回博客

为什么要大幅重写?

2024 年 1 月 23 日


上周是我在 Zed 的第一周。我加入的时候,团队正准备发布本周的 “Zed 2”——大事即将发生。这次发布标志着团队对 Zed 的 UI 框架 GPUI 从版本 1 到版本 2 进行了为期数月的重写 的结束。 所有人都在齐心协力,修复最后的错误,打磨 Zed,而我,作为一个新人,有很多问题:为什么要大幅重写?你们是如何完成的? 它能带来什么?你们是如何组织的?

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

接下来是我们长达一个小时的关于 GPUI 1 到 2 的重写、存在的风险、为什么 GPUI 1 必须改变、团队是如何围绕它组织的,以及代码的美感为什么重要的对话的编辑版文字稿。 我试图尽可能地保留意图和含义,同时消除那些嗯、是的、半句提问、回溯和过度交谈,这些构成了一次反思和深入的对话。

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

Thorsten:即将发布的 Zed 版本是一次重大的重写。 你们从 GPUI 1 切换到 GPUI 2。 我想问的问题是:为什么要大幅重写? 听起来这是一件耗时数月的大事,而且我们都听说过永远不应该进行大幅重写。 那么你们为什么要这样做?

Nathan:我同意。 我听说过,我知道。 但我想我是一个非常受重写驱动的程序员。 这是我流程的重要组成部分。

当您使用一个系统足够长的时间后,就很难逃脱该系统固有的基本 DNA:如果不是因为 B,我会修复 A;如果我已经修复了 A,我会修复 B。事情几乎陷入僵局。 决策因为其他决策而陷入僵局。

首先,我职业生涯早期学到的最重要的一课是,人们常常低估他们可以逐步实现的目标。 我就说这么多。 大多数开发人员最初的罪过是没有意识到你可以通过迭代的方式到达那里。 我当然仍然有违背这一点的倾向,低估了通过迭代可以实现的目标。

但我也认为,在您真正了解了一个问题,并通过构建 V1 学到了很多东西之后,拥有巨大的力量,可以通过一次斩断 Gordian 结的方式摆脱所有这些相互僵持的约束和负担,释放一切,像溶解系统一样,然后重新澄清其中的核心思想。

我认为我们承担了巨大的风险,通过增量方式,我们可能能够在更短的时间内使 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 代码中表达 UI 的方式(设置元素)和 TypeScript 代码(必须将样式应用于每个元素)之间造成了这种分裂。 最终,它并没有像我们希望的那样在动态样式方面发挥作用。 它太复杂了。

从理论上讲,我认为这听起来不错。 我们认为我们可以让你可以拥有 UI 主题,并且一些开发人员可以编写一个新的 JSON blob,该 blob 将完全重新设置应用程序中每个元素的样式,例如定义 Zed 的 Material 主题之类的东西,并且一切都将在运行时配置。

但最终在实践中效果很差。 团队中的每个人都有这种感觉。 在运行时加载东西并没有让我们更快地进行开发。

因此,我记得,在 Nathan 开始开发 GPUI2 之前,我的经验是我们需要重新开始假设在 Rust 中构建 UI 的工作流程应该是什么样的。

Thorsten:谁提出了重写? 有人说“让我们重写吧”,然后大家都说“让我们开始吧! 这听起来不错!” 还是存在怀疑?

Max:我认为我们处于 Zed 开发的某个阶段,我们已经开发了一个系统,然后做了很多工作,这些工作不涉及大量的按钮、表单和 UI 之类的东西。 它在很长一段时间内处于静止状态。

Nate,也就是设计师,习惯了这种情况,我们雇佣了一群人,他们去做不涉及新 UI 的工作。 因此,问题依然存在,但很长一段时间内无关紧要。

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

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

Nathan:值得一提的是,我一个人并没有制造主题灾难。 那是集体罪过。 [大家笑了]

但我负责布局系统。 并且,说实话,我有点从 Flutter 那里照搬过来的。 Raph Levien 多年前在他的帖子中给了我关注 Flutter 的想法。 但我从来没有完全理解 Flutter 的思维方式。 而且它也来自一种非常不同的语言,Dart。

我不知道,但我认为 Flutter 的很多工作方式都是基于对 Dart 这样一种快速编译的语言的假设。 然而,我们更像是在 Web 这样的场景中。 无论如何,对不起,我认为我有点跑题了。 我只是不想对我们最重要的事情造成的混乱承担全部责任。

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之上的GPUI2原型。GPUI1也有定义元素的方式,我们实际上使GPUI2元素成为一个有效的GPUI1元素。肯定有很多漏洞,但这至少允许Nate开始使用新框架,并快速验证这是否是一个值得去的地方。

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

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

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

但我认为我们可以承担少一些。视图方面的东西是一个亟待解决的尖叫问题,UI元素、布局等等的工作方式都是如此。

还有其他问题。最初的GPUI 1最初被设计为向Electron应用程序吐JSON。在Zed的最早期,最早的UI是一个启动Rust二进制文件的Electron应用程序。我们尝试了几种不同的方法。一种是直接运行一个子进程。然后,更早的时候,我们实际上将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:最初的估计是,我们将在10月底在意大利举行的团队峰会结束时完成。那是我们希望的,这太疯狂了,对吧?所以最终,我想,比最初的估计多花了两个月。但当我们越来越接近那个估计时,那还相当早,我们重新调整了目标,并将估计时间定为年底。我们确实达到了这个估计。

我知道如果时间再长,我们就不应该做了。是的,我就是这样想的。

Thorsten:Antonio,你说这不是你能否做到,而是需要多长时间和其他因素的问题。

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

我觉得峰会[在意大利举行的团队会议]对此非常有帮助。并且,使用 Zed 进行协作的能力是峰会上真正高效的关键。我们有三四个人组成的小组,在同一个工作副本上工作,更改和复制不同的文件。这也是一个非常好的时机,可以分享关于这个新框架的假设,什么是重要的想法,并达成共识。峰会之后,我觉得,我不知道,我认为事情进展得非常顺利。

Nathan:我觉得 Zed 几乎启用了一种完全不同的工作方式。Zed 加上 Rust。你实际上可以有四个人在一个工作副本中,并减少或消除成百上千个编译错误。

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

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

Nathan:是的,直到吃到意大利面之前,感觉都不是那么好。我一直在吃意大利面。意大利面真的解决了痛苦。但是,是的,感觉充满活力,每个人都了解了新的框架。这为我们接下来分开并独立工作奠定了基础。

Thorsten:回顾过去,你们三位,你们会说是成功的吗?

Antonio:我会说 100%。是的,这很有压力,我不知道这对我的健康,或任何人的健康来说是否是成功的。我不知道我们是否应该提及这一点。但我对代码的现状感到满意。所以我说是的。

Max: 我对现在的代码状态也很满意。 期间有些超级有压力的时刻——感觉就像往团队和代码库里扔了颗炸弹。 但很棒的是,它给了每个人一次形成凝聚力的经验。 他们说,他们参与了重建很多我们已经很久没动过的东西。

我认为,很明显,如果你拥有完美的知识,你或许可以规划出一条更有效的路线来解决我们想要解决的问题。 但你也可能在很多方面变得更糟。 你可能什么都没做。 然后我们可能仍然会继续沿着一条比实际需要更困难的道路迭代。 我们可能像 Nathan 说的那样,效率低下地、逐渐地、一次一个 PR 地走到今天。 那样也会带来挑战。

我认为 100% 是成功的,因为风险现在结束了。 我们将稳定它。 我们现在让它工作了。 而且我们解决了我们想要解决的问题,那就是构建视图很痛苦,而现在不痛苦了。 这花费了很多心血。 但我们任何人都无法在一开始就说,这里有一个更好的方法可以在更短的时间内解决这个问题。

Nathan: 是的,这是成功的。

Max: 我记得在重写开始的时候,我和团队里的不同人进行了一对一的谈话。 那时我们告诉所有人,“嘿,构建 UI 有点难,我们需要做一些改变。 Nathan 正在研究一种全新的元素系统。” 当时,我就是这么认为的:我们将改变 GPUI 中元素的工作方式。 即使那样,当时也感觉像是一项非常大的事业。

所以在 1on1 中,人们问如果仅仅改变所有元素的工作方式,这将如何运作。 但我已经和 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 年或其他时候开始发行一种加密货币。 做这件事的全部原因实际上更多的是自我表达。 而代码是这种表达的一部分。

现在,我认为这有一种失败模式,人们会为了代码本身的缘故而痴迷于代码,他们所做的只是欣赏他们有多少 monad 或其他东西。

但是对于代码来说,它是美丽的和精心制作的,我认为这是一件有效的事情。 只要它不变成一个小小的上帝。