每次重写都本该是最后一次

我把系统里同一个核心组件重写了三次。不是打补丁,不是重构——而是从近乎零开始彻底重建,整整三次。而每一次,我都确信这个版本才是真正的那个:干净、正确的架构,是我终于可以一劳永逸地在其上构建的那个。

我连续三次都错了,而且每一次都满怀自信。错本身令人恼火。但满怀自信才是我真正想谈的部分,因为那是同一种感觉的反复出现,而我从未有一次把它认作一个警告。

“这次它是对的”

一次全新的重写会带来一种特定的快感。旧代码一团糟——你能看到每一处妥协、每一个补丁、每一个现实逼你弯腰的地方。而那个干净地待在你脑子里的新设计,却没有这些。它优雅。它显而易见。它明显更好。

于是你拆掉旧的东西,建起新的,有那么一阵子它感觉像是进展,因为新代码确实更干净。然后你开始使用它,弯腰又开始了,过了一段时间,你站在第二堆烂摊子面前,感受着想要从头再来的那股一模一样的痒。

为什么重写总是看起来更好

这场比较是被做了手脚的,而我用了三轮才看清是怎么做的。当你评判一次重写时,你是在拿一个全新的、理想化的设计,去对比一个已经花了数月吸收实际使用所带来的摩擦的旧设计。新的当然看起来更好。它还没碰到任何一个问题。

旧代码看起来丑,不仅因为它设计得不完美,还因为它穿着它的阅历。它里面许多别扭的分支,都是真实边界情况留下的伤疤——是世界扔向它、而那个干净的新设计根本从未需要去挺过的东西。你并不总是在拿一个好设计跟一个坏设计比较。很多时候,你是在拿一个老兵跟一个新兵比较,并惊叹于这个新兵身上竟然一道伤疤都没有。

我一直在付的代价

每一次重写扔掉的不只是代码。它扔掉的是知识——那些经过积累、来之不易、对旧版本曾经悄悄学会应对的所有古怪情形的处理。而因为那些知识活在代码里而不是在我脑子里,重建就意味着重新发现它:再一次撞上那些奇怪的情况,再一次调试那些相同的问题,为我早已买过单的教训第二次、第三次地付钱。

修一个 bug,却意识到你以前修过这个一模一样的 bug,只是修在一个已经不存在的代码版本里——这里头有一种特别的疲惫。那构成了我第三次重写的大部分内容——慢慢地重新挣回我在第二次里扔掉的那些知识。

底下那个东西:过早抽象

所有这些重建真正的引擎,是我思考设计方式上的一个错误。我之所以一直重写,是因为我一直试图在真正理解问题之前就找到那个正确的结构。我把正确的抽象当成一种我只要够聪明、就能坐下来一次性正确设计出来的东西。

以我的经验,有用的抽象很少是一开始就被正确设计出来的;它们是在使用中挣来的。它们来自跟问题相处得足够久,久到学会了它真正在哪里弯折——哪些部分在变、哪些保持不变,真正的接缝在哪。提前把它完整设计出来就是在猜,而每一次重写不过是我又猜了一遍,信息略多一点,但仍远在我学到足够多、能猜得好之前。

一次重写究竟什么时候值得

我不想声称重写永远是错的。有时候一个设计确实无法伸展到你需要它去的地方,而原地演进它的代价会比从头开始更高。我最终学会去划的那条界限,关乎那股冲动的来源

由对问题的真正新理解所驱动的重写——“我现在看出,这个我当作一个概念来对待的东西其实是两个”——往往是真正的进展。而主要由对当前代码的不适所驱动的重写——“这太丑了,我想要点干净的东西”——则更常是穿了一层新漆的瞎忙活。前者更有可能产生有意义的结构性进展。后者大多只是改变你接下来几周对它的感受,直到那个新东西也变丑为止。

解法:原地弯它,直到你真的弯不动

我建立起来的习惯,是把重写从一种第一本能降级为一种最后手段。面对一个让我恼火的结构,我现在会先试着原地改它——重塑它、重构它,让它在我把它推向我所需要的方向时,保留住它积累的知识。我尽量把彻底重建留给那种现存结构真的在对抗每一次演进尝试的时候,而不仅仅是它冒犯了我的整洁感的时候。

很多时候,那股让你想从头再来的不适是信息,不是判决。那是代码在告诉你它哪里疼——而那恰恰是你会因从头重建而扔掉的知识。

那份笃定才是破绽

贯穿这三次重写的那条线索,不是糟糕的代码。它是一种感觉:那种干净、彻底的笃定,觉得这一次我终于对了。“这是最终的架构”,实际上就是我在每一次重写之前都说过的那句话。一句话不断被证明是错的,本该降低你对它的信任,而慢慢地,我确实降低了。

对一个你尚未经受压力测试的设计抱有强烈的笃定,并不等同于洞见。它往往只是那些本可教会你别样看法的伤疤的缺席——一个尚未与世界相遇的计划那种舒适的自信。如今,当我完全确信一个新设计就是最终的那个时,我会试着把这份感觉本身当作一个提示:我可能还没跟这个问题相处得足够久,久到能被信任去持有那个观点。

——没有信号,没有收益,不构成投资建议。