作为曾经的程序员,我犯下了这些尴尬的错误

阅读数:1548 2019 年 11 月 12 日 09:00

作为曾经的程序员,我犯下了这些尴尬的错误

有句话说得很有道理:“如果你以前写的代码不会让你感到尴尬,那你就没有进步”。40 多年前,我开始了编程生涯,30 多年前开始成为职业程序员,我在这个过程中犯了很多错误。作为一名计算机科学教授,我鼓励学生从错误中学习,无论是他们自己的错误、我的错误,还是经典的错误示例。我觉得是时候反省一下自己的错误了,让自己保持谦逊,并希望有人能够从中吸取教训。

错误第三名:微软的 C 语言编译器

在完成麻省理工学院大二的学业后,我开始在微软的 C 语言编译器团队实习,成了一个不折不扣的编程青少年。在做了一些繁琐的工作后,我着手编译器最有趣的部分:后端优化。具体地说就是改进 switch 语句的 x86 生成代码。

我当时居然想把我能想到的每种 case 都生成最优的机器码。如果 case 的值比较密集,我就把它们作为跳转表的索引。如果它们有公约数,我就利用公约数让跳转表更密集。如果所有值都是 2 的幂,我就使用另一种优化。

如果所有值都不满足任何一个条件,我就把这些分开,并递归调用我的代码。

真的是一团糟。

作为曾经的程序员,我犯下了这些尴尬的错误

多年后,我听说后来接手我代码的人讨厌死我了。

学到的教训

正如 David Patterson 和 John Hennessy 在《计算机组成和设计》(Computer Organization and Design)一书中所写的那样,计算机架构(也是软件工程)的一个重要原则是“优化常见的 case”:

优化常见的 case 比优化罕见的 case 更能提高性能,且常见的 case 往往比罕见的 case 简单。这个常识性的问题说明你知道常见的 case 是什么,而它们原本只有通过仔细的实验和测量才能获得。

我确实试图找出 switch 语句在实际当中是怎样的(有多少种 case,常量的分布式情况),但在 1988 年还没有这方面的数据。即使是这样,也不能说明当我在看到已有的编译器不能生成最优代码时就可以一直往编译器里增加新的 case。

我应该找一位有经验的编译器开发者聊聊,讨论一下常见 case 是怎样的,然后干净利落地处理这些 case。我本来可以写更少的代码,正如 Stack Overflow 联合创始人 Jeff Atwood 所写的那样,程序员最大的敌人就是自己:

我知道你的出发点是好的,我们都一样。我们是程序员,我们喜欢写代码,写代码是我们的职责所在。我们从来就没有遇到过一个无法用一撮代码解决的问题……

但大多数程序员很难承认这一点,因为他们非常喜欢写代码,但最好的代码其实是“无代码”。你写的每一行代码都需要经过调试,需要被阅读和理解,需要别人维护。每次在写新代码时,你都应该这么做,因为你已经用尽了其他办法。代码就是我们的敌人,因为我们写了太多该死的代码。

如果我写的那些用来处理常见 case 的代码很简单,那么维护起来就很容易,而不是留下一个没有人愿意(或不敢)收拾的烂摊子。

作为曾经的程序员,我犯下了这些尴尬的错误

错误第二名:社交网络广告

当我在谷歌从事社交网络广告方面的工作时,写了一些 C++ 代码,看起来像这样:

复制代码
for (int i = 0; i < user->interests->length(); i++) {
  for (int j = 0; j < user->interests(i)->keywords.length(); j++) {
      keywords->add(user->interests(i)->keywords(i)) {
  }
}

看到这段代码的程序员可能会看出这个问题:最后一个参数应该是 j 而不是 i。但当时单元测试没有发现这个错误,评审代码的人也没有发现。

这段代码在一天深夜里发布了,导致数据中心所有的计算机都崩溃了。

不过,这也没什么大不了的。因为代码先是在一个数据中心进行测试,然后才会被发布到其他地方。所以,只要让 SRE 回滚一些代码就可以解决问题。第二天早上,我收到了一封电子邮件,邮件中附带了堆栈崩溃的信息。我修复了代码,并添加了一些可以检测到这个错误的单元测试。

作为曾经的程序员,我犯下了这些尴尬的错误

学到的教训

有些人认为,犯这么大的错可能会丢掉工作。但程序员确实会犯错,但同样的错误不太可能一直犯。

事实上,我确实认识一个程序员,尽管他是一名优秀的工程师,但因为一个无心的错误而被解雇。后来他被谷歌录用(还升职了),谷歌根本不在乎他犯的这个错误,他在面试过程中也承认了这一点。

IBM 传奇董事长兼 CEO Thomas Watson 有一个非常有趣的故事:

当时有一个接近 100 万美元的政府大订单。IBM 公司——不,应该说是老 Thomas Watson——要求搞定每一笔订单。不幸的是,销售代表搞砸了,IBM 在竞标中失利了。那天,销售代表来到 Watson 的办公室。他坐下来,把辞职信放在 Watson 的办公桌上。Watson 看都没看一眼就知道那是什么了,这也正是他希望看到的东西。

他问:“发生了什么事?”

销售代表说明了谈判的每一个过程,说明了哪些地方出了错,以及他本可以采取哪些不同的做法。最后他说:“谢谢你,Watson 先生,你给了我一个解释的机会。我知道我们需要这笔订单。我知道这对我们意味着什么”。说完,他起身离开了。

Thomas Watson 在门口把他拦住了,并把辞职信递还给他,然后看着他的眼睛说:“我刚刚花了一百万美元给你上了一课,为什么还要接受这封辞职信呢?”

我的一件 T 恤上写着:“如果人们能从错误中吸取教训,现在一定可以拿到硕士学位了”。但实际上,我已经有博士学位了。

作为曾经的程序员,我犯下了这些尴尬的错误

错误第一名:App Inventor API

真正令人难堪的是有一种错误影响到了大量的用户,但却存在了很长一段时间,而犯下这个错误的人本来应该对它很了解。我犯下的最大的错误在所有方面都符合这种情况。

更糟即更好

90 年代,当我还是个研究生的时候,我看过 Richard Gabriel 写的“更糟即更好”这篇文章( https://www.dreamsongs.com/RiseOfWorseIsBetter.html )。我非常喜欢这篇文章,所以我把它推荐给了我的学生们。如果你还没看过这篇文章,现在就看吧,文章很短。

这篇文章从几个维度对比了“做正确的事”和“更糟即更好”的哲学:

做正确的事:设计必须简单,无论是实现还是接口。保持接口简单比保持实现简单更为重要。

更糟即更好:设计必须简单,无论是实现还是接口。保持实现简单比保持接口简单更为重要。

App Inventor

我是谷歌 App Inventor 团队的成员之一,App Inventor 是一个在线编程环境,让初学者能够通过拖放的方式创建 Android 应用程序。

早在 2009 年,我们就急于发布 alpha 版本,这样就可以赶上夏季的教师研讨会,并在秋季的课堂上使用。我自愿要求实现 sprite。当时我天真地回忆起年轻时用它们在 TI-99/4 上开发游戏时的情景。sprite 是一个 2D 对象(比如宇宙飞船、小行星、球和桨),可以移动,并与其他程序元素发生交互。

我们用 Java 实现了 App Inventor,它本身就是面向对象的,所有东西都是面向对象的。由于球和图像 sprite 在行为上非常相似,我创建了一个抽象类,带有一些属性,比如 X、Y、Speed 和 Heading。它们有一些常见的方法,比如碰撞检测、从屏幕边缘反弹等。

球和图像 sprite 之间的主要区别是绘制的内容不同:球或位图。因为我先实现了图像 sprite,所以很自然地让 x 和 y 坐标作为图像放置的起始位置。

作为曾经的程序员,我犯下了这些尴尬的错误

在实现了图像 sprite 之后,我认为只需要一点额外的代码就可以实现球 sprite 对象。问题在于我实现的方式太简单了:X 和 Y 轴坐标指定了球的左上角位置。

作为曾经的程序员,我犯下了这些尴尬的错误

但其实我应该用 X 和 Y 轴指定圆心的位置,就像数学书里教的那样。

作为曾经的程序员,我犯下了这些尴尬的错误

与我犯下的其他错误不一样,其他错误主要影响到了我的同事,但这个错误却影响到了数以百万计的 App Inventor 用户,其中有很多是儿童或编程新手。他们不得不在每个使用了球组件的 App 中做一些额外工作。虽然我可以一笑置之,但我真的为自己犯下的错误感到羞愧。

十年后,也就是在最近,我给这个错误打了个“补丁”。我说“打了个补丁”而不是“修复”,因为正如伟大的 Joshua Bloch 所说的——“API 是永恒的”。我们不能做出任何会影响现有程序的改动,所以我们添加了一个叫作 OriginAtCenter 的属性,它在旧程序中默认值为 false,在未来会改为 true。如果用户想知道为什么球的原点总是不在中心位置,答案是:十年前,一个程序员因为偷懒没有把 API 设计好。

学到的教训

如果你需要开发 API(几乎所有的程序员都需要),应该遵循最佳实践,你可以从 Joshua Bloch 的视频“如何设计一个好的 API 以及它为什么这么重要”( https://www.youtube.com/watch?v=aAb7hSCtvGw )中学到一些东西,或者看看这篇总结性的文章( https://www.infoq.com/articles/API-Design-Joshua-Bloch/ ),其中包括:

API 可能是你最大的资产或负债之一。好的 API 能够长久地留住用户,不好的 API 会成为长久的维护噩梦。

公开的 API 就像钻石一样,是永恒的。你只有一次机会把它做好,所以要全力以赴。

早期的 API 草稿应该尽量简短,通常是类、方法签名和一行描述。这样,如果没能一次性设计好 API,就可以很容易地进行重新调整。

在实现 API 之前,为它们编写测试用例代码,这样可以避免实现不好的 API

如果做了以上这些事情,我可能就会发现设计方面存在的错误,并将其修正。如果没有,或许我的其他队友会。任何一个可能与人们伴随多年的决策都值得至少花一天时间去思考(不管是否与编程有关)。

Richard Gabriel 的文章标题(“更糟即更好”)是说,即使产品有缺陷,也要率先推向市场,而不是老想着创造完美。然而,当我回看 sprite 代码时,我发现用正确的方法做好事情并不需要更多的代码。无论如何,我当时做了一个糟糕的决定。

结论

程序员每天都会犯错误,无论是代码错误还是尝试新事物失败。成为一名程序员不一定都会犯跟我一样严重的错误,但如果不承认错误并从错误中学习,就不可能成为一名优秀的程序员。

作为一名老师,我经常遇到一些学生,他们担心自己不适合学习计算机科学,因为他们会犯错,而且我知道“骗子综合症”在技术行业十分盛行。我希望你们会记得这篇文章中提到的经验教训,但我最希望你们记住的是,不管是笑、还是哭,我们都会犯错。

原文链接:

https://stackoverflow.blog/2019/10/29/my-most-embarrassing-mistakes-as-a-programmer-so-far/

评论

发布