Grow @ Google 02: 「能用就行」还远远不够

在「大厂」工作的体验就像当一颗螺丝钉?读过很多软件工程的书却仍会犯下里面的典型错误?感到自己工作效率低下却不知如何改进?要成为一名高效的软件工程师,在理论和实践之间是存在不小的距离需要我们通过广泛学习、勇敢试错和不断反思去跨越的。在 Grow @ Google 这个系列中,我会结合自己在 Google 的工作经历来分享一些经验和教训,帮助大家少走一些弯路。系列中的其他文章请见:


我们的目标是:没有代码

首先让我引用一句出处不详1的话:

Source code is a liability, not an asset!

站在工程师的角度,代码最终是为解决实际问题服务的。不论它们有多么优美、能给人带来多少理性愉悦,更多的代码就意味着更高的维护成本和更多的潜在 bug。

能够明确和认同这一点,在我看来是作为一个工程师迈向成熟的标志之一。这意味着不再对代码本身持有过度甚至病态的执着,也就能够不带(关于编程语言、工具链和技术栈等的)偏见地去欣赏特定场景下特定解决方案的美。

世界上有两类工程师

《A Philosophy of Software Design》是继《The Effective Engineer》之后又一本我相信每个工程师都应该读的书。这本书主要讨论了软件工程中的复杂性(Complexity)从何而来,以及如何降低一个软件系统的复杂度。书中对比了两类不同的工程师在解决问题时会采取的不同思路:Tactical Programming 和 Strategic Programming。

第一类工程师就像战术家,只着眼解决当下的问题:可能是抢修一个 bug,又或者是扫除新 feature 上线的障碍。为了在最短时间内达成目标,他们往往会放宽对代码质量的要求,或是违背工程上的最佳实践;第二类工程师就像着眼全局的战略家,能持之以恒地投入资源从而在完成开发目标的同时逐步地、系统性地提升整个 codebase 的可维护性、可拓展性以及测试覆盖率等。

第一类工程师将复杂性和技术债务带入软件系统,而第二类工程师则是我们应该效仿的对象。在现实中,第一类工程师却往往是多数。我们总能找出理由来为自己辩解,比如「能用就行了」,又比如「上一个维护者的代码就写得稀烂,我没有义务去为了下一个维护者的开发效率而牺牲自己的时间」。然而,这样看待问题的结果往往是搬起石头砸自己的脚。

试想你开发了三个月的项目上线在即,PM 突然告知需求有改动。你一边在心里问候 PM 一边翻阅新的需求,发现得在代码层面上做比较大的重构。这时如果先前的设计使得系统的一些行为难以修改,或是没有投入足够的时间来写全面的测试,这个重构可能就会大大推迟项目的上线时间,甚至发展成不得不重写部分模块的情况。没有谁能打包票说自己绝不可能遇上这样的情况。所以,即使仅从自己的利益出发,多用 strategic 的视角考虑问题也是有益处的。以下是基于个人经验的一些具体操作建议:

  • 新代码必须有测试。对于缺乏测试的旧代码,改动时的第一步永远应该是添加测试。
  • 在改动一个模块之前,如果观察到代码质量不理想,可以考虑先做一次重构。当然,良好的测试覆盖是我们能够充满信心地重构的先决条件。
  • 充分利用各类代码分析工具,定期发起 Fixit 来提高代码性能和可读性、优化 build 规则、改进易错代码、补充注释、处理 todo、清理 dead code、迁移 deprecated API、拆分变得过于庞大的类和文件等等。
  • 如果开发的痛点是由不够理想甚至错误的 design 所导致的,在寻找 workaround 作为短期解决方案的同时,也要开始思考、规划和投入资源在重构或是迁移到更正确的 design 上。

当然,Tactical Programming 和 Strategic Programming 之间并不是完全对立的关系。除了由于迫于生计而不得不在工程质量和开发速度之间寻找平衡以外,我们对一个实际问题的认知也是在不断发展的。在信息不充分的前期,积累一些技术债务是不可避免的。学会定期偿还这些债务以保持系统的可持续开发,则是成为高效的工程师的关键。

重要的事情说两遍

在这个系列的一篇文章中我写道:

高效的工程师们有一个共性是会强迫症般地投入时间来缩短 feedback loop,从而达到更快的迭代速度。但一个常见的误区是,在身上已经背满工作时,我们最不想做的一件事情就是给自己再创造更多的工作。如果已经存在一个可用的 workflow,即使中间有可以优化的部分,很多人都会倾向于不加改进地继续用下去。

事实证明,这个误区的确是很难彻底摆脱的。从那时以来,我又犯了好几次类似的错误。究其根本,缩短 feedback loop 往往需要先做一些看似比较大的时间成本投入,人在做决策的时候则往往只关注眼前的问题,从而低估了一笔一次性的投入能带来多少的长期收益。

以我最近一次跨入这个误区为例,我在使用了十几次一个耗时 5 分钟的测试流程后幡然醒悟,花了约 2 小时将这个流程优化到了耗时 5 秒钟。只要新的流程能被使用超过 24 次,这笔投资就可以回本。很显然,24 是一个轻易就会被超越的数字,更不用说优化的成果对团队的其他成员以及未来的维护者们也有帮助。

以下是一些更具体的技巧,希望能对打消大家对先行投入时间来缩短 feedback loop 的抵触有一些帮助。

  • 在做测试时,工程师有一个得天独厚的优势就是可以修改代码来创造实际产品中不存在的「捷径」。比如重现一个 bug 需要打开网页并在 UI 上点击 5 才能到达某个状态,我们就可以修改代码使得网页一打开就处于这个状态。
  • 分析和思考工作流程中存在的瓶颈,将无法避免的等待转移到耗时更少的地方。比如编译和部署一个新的 binary 需要一小时,而 push 一个新的配置文件只需要 5 分钟,那么就应该用配置来控制所要测试的代码分支,而不是将逻辑 hardcode 进去。
  • 对于使用频率越高的工具,越要提升熟练度,比如日常使用的编辑器、IDE、开发工具链等。多用键盘,少用鼠标。
  • CI/CD、增量编译、hot reload 等理应成为开发的标配。如果你的工作环境中还没有这些工具,越早补全就越能节省整个团队的时间。
  • 单元测试的颗粒度要尽量低,并且所使用的测试框架要支持测试单个 case,这样才能花最少的时间成本验证你的改动。

对孤军奋战说不

很多人可能都听说过这样一个说法:如果你是一个团队里唯一懂某件事情的人,这就会成为你的核心竞争力,使你成为不可被取代的人。

而如果你是一个像我一样在科技行业的所谓「大厂」工作,觉得自己的日常是在当「螺丝钉」或是「润滑油」的工程师,这个说法就不适用了。做团队中唯一熟悉某个模块(服务、工具、专门领域……)的人并不见得会是一件好事。因为:

  • 工作很少会全程一帆风顺,每个项目都有高光和低谷的时刻。如果团队中其他人不熟悉你的工作,在低谷时你就更容易感到士气低落,而在高光时也更难与团队分享你的喜悦。
  • 你可能会不得不负责所有跟这个模块相关的工作,包括那些你不乐意干的活。当你想转变工作方向、尝试其他类型的项目时,这可能就会成为一种束缚。

跟许多同行一样,Google 对工程师的基本要求之一就是能够独立完成工作。但与此同时,我们也会通过 design review、code review 甚至闲暇聊天的方式去有意识地跟 peer 们 share context。这样做有许多好处:

  • 避免上面提到的那些孤军奋战的负面效果。
  • 有第二双眼睛用不同的视角来帮你看事物总是好的,尤其有助于验证你所做的一些根本的假设是否成立。2
  • 跟 peer 沟通的过程会迫使你思考(尤其当沟通是以书面的方式,或者你需要「教育」peer 一些背景知识时),从而加深自身对问题的理解,还可能捕捉到自己思维中的漏洞。
  • 帮助提升你的工作的 visibility。

杂谈

  • 可能的话,适当挑选要做的项目,使它们处于不同的阶段。比如项目 A 在设计阶段,项目 B 在火热开发中,项目 C 已经 code complete 正在走上线流程。
  • DRY (Don't Repeat Yourself) 原则在测试中不一定适用,不妨采用 DAMP (Descriptive And Meaningful Phrases) 原则3
  • 勇敢地一头扎进完全不熟悉的代码中。能做到这点的工程师往往能取得更多的成功。

Notes

  1. 我最早是从朋友 Yiming 那里了解到这个说法,但相似的表达有着许多版本,原始来源也很难考据。一个时间上比较早的版本见这里
  2. 我在《Grow @ Google 01: Noogler 成长的必经之痛》中讨论过一些错误假设及它们的后果。
  3. 两者不是对立的,详细解释见这里

Subscribe via RSS

CC BY-NC-SA 4.0 © 2019 - 2020 ❤️ Linghao Zhang