酷炫的工具具有诱惑力。当我们思考软件生产力时,工具自然而然地浮现在脑海中。当我们看到漂亮的新工具时,我们倾向于相信它们惊人的功能将帮助我们更快地完成工作。因为每个软件工程师每天都在使用软件生产力工具,并且所有团队经理都必须决定他们的成员将使用哪些工具,所以最新和最棒的工具看起来很有吸引力。
软件工程师和经理常常低估我们每天使用的“心理工具箱”。你的大脑对生产力的影响可能比你购买或下载的任何工具都大。编程语言、编译器、调试器、集成开发环境 (IDE)、静态代码分析器、动态代码分析器、性能监视器、测试框架、项目管理系统和缺陷跟踪系统:所有这些都很好且有用,但如果没有明确的目标和有效的工作实践,你还不如在敲击石头。
我观察到,我们的社区在软件工具上的花费比在培训和技能发展上的花费更多。然而,如果我反思自己的经验,多年来我掌握的许多最有用的技能都不是在学校或公司培训课程中学到的。其中许多技能非常简单,应该是显而易见的选择,但我不断惊讶于有多少工程师和经理没有意识到它们。我想知道自己错过了多少其他“显而易见”的技能?
在我的职业生涯早期,我用 Icon 编写了一个会计软件包,Icon 是我刚刚发现的一个很棒的新工具,它具有 Fortran、C 或其他我熟悉的语言所不具备的许多功能。我的经理要求我编写一个程序,汇总可计费的连接时间、CPU 使用率、磁盘消耗和其他数字——并用收费值进行总结。他没有指定编程语言,所以我决定使用 Icon。
正如你可能想象的那样,这个项目花费的时间比我预期的要长。我学到了很多关于 Icon 的知识,尽管这不是我经理的首要任务(事实上,他不知道我在使用它)。因为 Icon 有一个解释器,构建和测试我的代码比使用 C 要快,但我编写 Icon 的速度比 C 慢得多,因此构建时间的改进并没有带来太大的改变。此外,Icon 的标记处理功能比 C 更先进,因此当我学习新的 Icon 习惯用法时,我花了很多时间编写和重写代码。当我完成项目时,我是办公室里唯一一个了解 Icon 的人——而且我只是一般了解。我也是唯一一个可以维护该软件包的人。当我离开后,该软件无法修复或升级,其他人必须编写一个新的会计软件包。Icon 是一个很好的工具,非常适合我给它的任务,但结果是生产力降低了。
问题是什么?我的目标错了。我的主要目标应该是编写我的团队可以维护的软件,而不是学习 Icon。因为我再也没有在后续项目中使用 Icon,所以最终这个会计软件包基本上对我经理和我来说都是巨大的时间浪费。
这里有几个教训。显而易见的一个是,如果你有错误的目标,那么工具再酷也没用。不太明显但同样重要的教训是,你为其他人编写代码——而不仅仅是为编译器。
我大约花费百分之一的编码时间来从头开始设计和编写原创代码,而轻松花费剩余的 99% 的时间来修复现有代码。代码维护是我编码时间的最大部分;为了大幅提高我的生产力,我需要更快地修复错误或处理错误较少的代码。如果我正在修复的代码编写时没有考虑到其他人能够理解,那么我可能会花费大量时间在上面。因此,无论我是从头开始编写代码还是修复现有代码,我的主要目标都必须是减少我的维护负担。如果这不是一个目标,那么世界上最好的生产力工具只会更有效地浪费我的时间。
当然,选择不当或不明确的目标可能会以其他方式降低生产力。以下是一些因错误目标而导致生产力下降的例子
其中一些例子更可能影响管理者,而另一些则对工程师影响更大。如果代码永远不会被使用,或者如果它会导致维护噩梦,那么这真的不是代码生产力。
作为计算机科学专业的本科生,我有一种有趣的编程风格。我会把我的作业拿到穿孔卡片机旁(即使在当时,它们也是古董,我当时并不知道),然后开始漫无目的地打孔。操作员会拿走我的卡片组,通过 Algol 或 Pascal 编译器运行它,并递给我一长串错误列表。我会修复一些错误,然后一遍又一遍地尝试,直到它编译通过。当程序或多或少运行时,我会尝试找出它哪里出错了并修复它。这通常需要很长时间,并且消耗了大量的穿孔卡片。许多树木在这个过程中死去。
我使用了糟糕的工具和糟糕的编程风格。然而,在我的学生生涯中,我的生产力工具逐渐得到改善。我开始坐在“可视化”编辑器前,漫无目的地在一个源文件中输入代码。我会运行 Pascal 或 C 编译器,并反复修复错误,直到程序再次基本工作。然后我会进行修改,添加功能和修复错误,构建和重建。我会添加大量的打印语句来告诉我程序的哪些部分没有工作。
据我所知,在那些日子里,我的所有朋友都使用了完全相同的编码技术,我称之为“凑合到能用为止”。这非常耗时(即生产力低下)。完成的代码通常包含讨厌的潜在错误。新工具让我能够更快地凑合,但我的代码仍然很糟糕。
随着我的技能的改变,我的代码变得不那么糟糕了。通过反复试验、观察和模仿,我不知何故设法掌握了一些良好的工作实践。其中大多数都非常简单,可以在不使用尖端工具的情况下执行;当我被迫使用别人的工具时,这派上了用场。有些是在学校里有人向我建议的,尽管我当时忽略了它们。大多数是在工作中观察我尊敬的人时学到的。
过了一段时间,我发现了一些了不起的事情。我的程序编译错误少了很多。小型程序有时第一次就能正确运行——在经历了多年的凑合到能用为止之后,这真是令人震惊。当我后来回到这些程序时,我实际上理解了它们是如何工作的。当我尝试修复别人的程序时,修复是成功的,代码是可读的。我仍然不是世界上最好的程序员,但自从我职业生涯的早期阶段以来,我的生产力已经飞速提高。
以下是我在职业生涯中掌握的一些良好工作实践的列表,以大致递减的重要性顺序呈现。对我有用的东西可能并不总是对你有效,但其中一些实践非常简单和显而易见,以至于当我发现我的同事没有使用它们时,我常常感到困惑。
1. 保留完整的开发笔记和调试日志。 我广泛使用实验笔记。现在这种做法已经根深蒂固,我无法想象以前我是如何生存的。我的笔记都在网上,所以我可以使用模式匹配工具搜索它们,而且因为我打字比写字快。这种做法给我带来了生产力的最大提升,减少了试图回忆我一年/一个月/一周/昨天做了什么的时间。始终记录你的突破和死胡同,这样你就无需重新发明它们。实验笔记是一个惊人的生产力工具,许多优秀的程序员从未使用过。
与实验笔记密切相关的是调试日志。你可以通过多种方式跟踪调试笔记:手动注释你的调试经验;将调试材料剪切并粘贴到实验笔记文件中;在脚本环境中运行面向行的调试器;或者三者都做!有了日志,你永远不需要重新创建调试情况只是为了看到相同的数据,并且你应该能够稍后在日志中搜索特定的名称或值。
2. 使用大纲。 高级结构不是代码的涌现属性。如果你不从一开始就拥有高级结构,那么你的代码中出现高级结构的可能性类似于猴子敲击打字机复制莎士比亚的可能性。痛苦的经验使我成为一名自顶向下的程序员;我现在总是先勾勒出高级组织和数据结构,然后再尝试攻击任何编码项目。但我仍然经常看到代码显然是凑合到(某种程度上)能用为止的。
一位前同事曾经通过直接依次编辑每个源文件,添加代码来操作数据结构和映射数据,从而解决了一个重要的操作系统问题。他修复了所有编译错误并开始调试。新代码反复破坏了他的磁盘内容。一个月后,他没有任何进展,所以他扔掉了旧的“原型”并重新开始,使用了完全相同的编程技术,但从经验中学习。较新的代码反复破坏了他的磁盘内容,尽管没有旧代码那么频繁。调试较新的代码仍然很困难和令人沮丧。过了一段时间,他也把代码放在一边,去做其他事情了。他的代码从未成为系统的一部分。他在整个项目期间的净生产力为零。
大纲本可以帮助解决这种情况。以下是创建大纲所需的基础知识
对于新代码
对于旧代码
所有这些工作都应该在添加或修改任何一行代码之前完成。有了大纲,你不仅会在开始编码时看到代码的高级结构,还会有一个具体的目标去努力。
请注意大纲如何产生一种分形扩展,扩展成更多的大纲。最终,这些大纲将开始看起来像代码。这就引出了伪代码的主题。
3. 编写伪代码。 我是一个相当不错的程序员;我理解多种编程语言的正反两面。但是编程语言不是自然语言——我用自然语言思考。伪代码是我自然语言和目标编程语言之间的中间步骤。它帮助我组织我的思想,使其易于阅读和转换为编程语言。当我处理别人的代码并且在理解逻辑方面遇到一些麻烦时,我会将代码转换为伪代码,以测试我的理解并提高可读性。即使是小型项目也受益于伪代码——“啊哈,原来它是这样工作的!”
我认识很少有其他工程师使用伪代码,所以这里有一些基本知识
高级伪代码
为给定的优先级分配一个新元素
按优先级以数字顺序将其插入到全局列表中
低级伪代码
分配一个新元素
如果我们失败,
报告错误
初始化优先级字段
初始化线程队列
遍历全局列表
如果元素的优先级超过新优先级,
停止
如果我们没有找到优先级更高的元素,
将新元素插入到列表末尾
否则
将元素插入到优先级更高的元素之前
C 代码(使用 <sys/queue.h> 宏)
if ((new_prioq = malloc(sizeof (*new_prioq))) == NULL)
return (-1);
new_prioq->priority = new_priority;
STAILQ_INIT(&new_prioq->threads);
TAILQ_FOREACH(prioq, &prioq_head, chain)
if (prioq->priority > priority)
break;
if (prioq == NULL)
TAILQ_INSERT_TAIL(&prioq_head, new_prioq, chain);
else
TAILQ_INSERT_BEFORE(prioq, new_prioq, chain);
你不必写完整的句子,但你的伪代码应该让你能够以舒适的习惯用法阅读和理解你正在编码的函数,并紧密映射到目标编程语言中的语句。
4. 在工作前学习。 别人可能以前就处理过你的问题。甚至可能就是你。
这种做法与“非我发明”相反。如果有人以前做过,并且做得还不错,那么再做一遍就是浪费时间和生产力。世界不需要另一个哈希数据库访问例程,你的经理也不想看到你编写或调试它。即使你认为你会以稍微不同的方式完成同样的工作,你仍然会从了解其他实现方案以及它们的工作原理中受益。
查看本地文档。在我的系统上,我可以运行
$ man -k hash
[...]
hash (3) - 哈希数据库访问方法
[...]
$
查看网络。向 Google 询问可扩展的哈希函数。如果获得的结果太多,请优化搜索。网络可能是一个惊人的生产力工具。阅读手册并使用在线帮助。对于棘手的问题,阅读期刊、会议记录和论文。许多期刊和会议记录也在网上。不要重新发明,要重复使用。
5. 使用代码审查。 编译器可以告诉你代码是否使用了正确的语法,有时还可以告诉你是否使用了常见错误迹象的构造。编译器无法告诉你的是代码是否完成了你想要它做的事情。这就是审查者的作用;他们提供第二双眼睛来阅读你的代码。当你的代码在你眼前停留一段时间后,你会失去视角。一些组织现在使用正式的检查,这很棒,但即使是朋友的简单通读也可以发现大量的错误。当你将代码提交给任何审查时,谦虚是一种有用的技能,因为你不可避免地会惊讶于自己会搞砸多少事情。让每一行代码都经过审查可能是不切实际的,但至少提交你的高级设计和关键路径是至关重要的。
我最近为一个受支持处理器的新型号编写了一些内存管理单元 (MMU) 支持。正式检查发现了 32 个缺陷。大多数缺陷报告都要求注释或其他修饰性更改,以使代码更易于阅读。这种反馈非常有用:到目前为止,除了我之外,没有人看过这段代码。编译器永远不会提出这样的缺陷列表。当然,当编译器在我的代码中发现缺陷时,我从来没有感到尴尬,但我不得不学会当审查者在我的代码中发现问题时不要感到尴尬。不把它放在心上真的很有帮助。在我的代码构建和测试完成时,与未经审查的代码相比,它的缺陷会少得多,并且其他人将能够理解和维护该代码。
6. 使用版本控制。 版本控制被认为是一种美德,以至于几乎每个项目今天都在使用它。这是一个很好的例子,说明好的工具确实可以发挥作用。然而,许多人仍然没有有效地使用版本控制。根据我的经验,最糟糕的问题是由那些不想在代码完美之前检入代码的工程师以及那些抵制编写信息丰富的日志注释的工程师造成的。第一种情况是备份问题的一种变体:不经常备份的人正在将他们的生产力置于巨大的风险之中,因为他们可能不得不重新创建他们的工作。第二种情况几乎同样糟糕:如果你无法弄清楚为什么有人做了破坏程序的更改,你就无法修复它。
这是一个糟糕的版本控制日志条目
已解决 SPR 78560
这是一个更好的版本控制条目
修复了 malloc() 中的一个问题,该问题导致它返回未自然对齐的存储。
已解决 SPR 78560。
由 Chris Torek 审查。
7. 使用良好且一致的编码风格。 最好的编码风格是在项目的其余部分中使用的风格,这确保了必须修复代码的人可以轻松阅读它。我花了多年时间才摆脱我对个人、独特的 C 编码风格非常棒,我可以将其用于一切的痴迷。毕竟,编译器不在乎我的编码风格。问题是,其他人会被我的代码绊倒。它阅读起来很慢,这降低了他们的生产力。我现在尝试精确地模仿我正在修复的程序使用的编码风格,或者——对于原创编码——我的软件团队正在使用的风格。当你编码时,你是在为一群人而不是计算机编写代码。如果不是这样,你还不如用二进制编写。
毫不奇怪,我对编码风格仍然有一些痴迷。我最重视的是,人们应该尽力编写每个人都可以直接阅读的代码,而不是每行都需要解释性注释的代码。有时你正在使用 PowerPC 汇编语言,例如,即使添加大量宏也无法使其可读。但是,如果你使用典型的高级编程语言编写代码,则可以为操作(函数)和数据(变量)选择自己的名称。如果可能,在不同函数中具有相同用途的局部变量应该具有相同的名称——不仅在同一个源文件中——而且在整个项目中。如果人们在阅读你的代码时不得不抓头挠腮,生产力就会下降。
这个列表有些删节,因为我已经从列表中删除了十几个项目,但你明白了我的意思。在每种情况下,工具都可以提供帮助,但是如果使用工具的人不致力于良好的实践,那么工具可能会变得无效甚至无用。
在谈论了这么多关于人们可以做些什么来提高自身生产力之后,关于使用工具的最佳方式,我能说些什么呢?
1. 最好的工具是那些你有效使用的工具。 无论你拥有多少工具,或者它们有多么酷炫,最好的工具都是你和你的团队有效使用的工具。对于复杂的工具,大多数人只使用其功能的一个子集,但重要的是该子集的有效性。(未使用的功能只是浪费了工具设计者的生产力。)许多工具集完全可以满足程序员完成任务的需求,因此添加功能可能不会改变程序员的工作方式。
让我们看一下最好且最易于使用的工具之一:编译器警告。我目前使用的编译器可以生成各种警告,其中一些比另一些更相关。然而,许多程序员对误报或使特定警告消失所需的代码更改感到非常恼火,以至于他们禁用了所有警告。我购买和下载了各种源代码,这些源代码在编译时会产生大量的警告,但只有一定数量的警告指出了代码中的严重问题。但是,只有当你保持启用编译器警告时,编译器警告才是一种有效的工具。我多年来的感觉已经改变,我认为好的编译器警告非常有价值,以至于为了避免误报编译器警告,防御性编程是值得的。我改变了我的一个做法,结果提高了我的主要工具之一的有效性。
我曾经被要求移植一个庞大的程序,并在一个模块中遇到了一些令人讨厌的编译困难。该模块是用 C++ 编写的,并且大量使用了模板,模板在当时是一项尖端功能。代码几乎无法阅读。这位程序员显然比我对 C++ 更精通,可能在许多特殊的 C++ 功能方面有经验。然而,我的 C++ 编译器和调试器不够新,无法处理新的模板语法,所以我被迫升级它们。然后我能够构建程序,但是该模块失败了。我花了很多时间试图弄清楚是编译器还是代码出了问题。代码实际上不需要尖端功能,但是这些功能很酷且复杂。我的生产力直线下降,因为我被迫生活在尖端。虽然你可以争论 C++ 的无数演进功能中哪些是值得的,但事实仍然是,在生产程序中使用尖端功能始终是一个非常糟糕的主意。新功能对我来说根本无效。
有时你被迫有效地使用你没有选择的工具。我是一名高级程序员,所以我很幸运能够指定很多工具,但不是全部。大多数时候,当你参与新工作或新项目时,团队成员已经知道他们将使用哪个操作系统、编程语言、编译器、调试器等。其他人已经为你选择了你的工具,因此你需要使这些工具在你的工作中对你有效。当然,你总是可以向你的经理推荐更好的工具,但是如果你正在等待更好的工具到来,你的生产力就为零。
然而,也可能走向另一个极端。一个团队或其管理层可能会不断升级到最新最好的工具,并认为新功能可以提高生产力。如果程序员永远无法充分熟悉每个新工具,那么新工具可能无效。
2. 糟糕的工具到底有多大伤害? 少数工具集是积极有害的。经典的 Ed 编辑器错误消息就是一个例子:它为几乎每个用户错误都打印 ?。一个好的工具应该促进新手的学习,并且对专家来说也非常高效。我见过许多糟糕的工具的例子,它们在这两个方向上都过于偏颇。有些工具非常关注手把手地指导我,以至于我永远无法有效地使用它们。我发现文字处理器经常以这种方式冒犯我。其他工具过于关注专家使用(或程序作者的使用),以至于对我来说完全不透明。在任何一种情况下,我都可能最终不会采用该工具,如果被迫使用它,我可能永远不会高效地使用它。这样的工具会损害生产力。
有些工具只是在技术上存在一些缺陷——它们不能像广告宣传的那样工作。这些工具会浪费生产力,因为你花费时间编写错误报告(或对着屏幕大喊大叫)。当编辑器每天崩溃,让你失去最后一小时的工作时,你的生产力会直线下降(并且你会寻找新的编辑器)。我工作过的一个系统有一个半透明文件系统功能,这对于构建发布非常方便。(使用半透明文件系统就像在描图纸上绘图:它提供了只读文件系统的视图,你可以在不影响底层数据的情况下对其进行添加和更改。)它在大多数情况下都有效,但偶尔会严重搞砸事情。我们不得不重新创建数据以从错误中恢复,这花费了很长时间。该功能带来的生产力提高被其错误导致的生产力下降所抵消,因此我们在发布生产系统中放弃了它。
我的总体经验是,真正糟糕的工具集很快就会被淘汰。软件工程团队非常善于让成员了解糟糕的工具。有时,工作要求使得避免使用糟糕的工具成为不可能。在这种情况下,你必须制定应对该工具的策略,因为(消极抵抗地)拒绝使用它会损失更多的生产力。
更常见的是,我发现自己处理的是平庸的工具而不是积极有害的工具,并且我通常可以找到与它们合理高效地工作的方法。因此,我对糟糕的工具是否会造成伤害的回答是:“是的,但是……”
3. 好的工具也会既有帮助也有伤害吗? 这是一个棘手的问题。如果一个工具真的很好,那么理论上它不应该损害你的生产力。有时一个工具擅长它所做的事情,但它对必须使用它的人来说并不好。一个工具可能运行速度很快,但会给用户准备数据和解释错误带来很多痛苦,而另一个工具运行速度较慢但更易于使用。生产力是通过你的团队完成的工作量来衡量的,其中包括人员时间。人员时间通常比计算机时间更昂贵,并且使用笨拙的工具,人员时间可能会在很大程度上超过工具运行时间,以至于工具的速度变得无关紧要。
有时好的工具可能会被无效地使用。用花哨的新工具替换旧的平庸工具会导致生产力下降,因为你的团队需要掌握新工具。如果这种生产力损失在项目的整个生命周期内没有得到弥补,那么即使新工具是一个好工具,它也可能是净生产力损失。Emacs 比 Ed 更好,但如果你要求你的团队在项目进行到一半时切换编辑器,你可能会错过你的截止日期。
新工具的某些功能可能看起来不错,但实际上是无效的。例如,一个新的 IDE 减少了编译/调试/编辑周转时间,但它优化了错误的问题。程序员的时间被编译/调试/编辑周期主导,他们正在使用低效的工作实践——他们需要使用在错误到达编译器之前减少错误的做法。当你到达调试器时,生产力之战可能已经失败了。同样,可视化工具可能会生成非常酷的绘图,但是如果用户不理解和内化视图,则该工具是无效的。(参见 Edward R. Tufte 的著名著作《定量信息的视觉显示》,第 2 版,Graphics Press,2001 年。)
总之,如果你不考虑人们如何使用好的工具,好的工具也会造成伤害。
4. 为什么人们总是要求工具中提供更多和更花哨的功能? 部分原因在于文化问题。我们喜欢酷炫的玩具。这是我们做我们所做的事情的部分原因。部分原因是营销问题。公司必须改进他们的产品,以便他们可以继续从中赚钱。开源软件也是如此,贡献者必须不断“改进”软件以展示他们的能力和酷炫。然后,公司或开源项目向我们推销酷炫的新改进的软件,我们购买或下载它。有时市场会抵制。看看微软 Windows 3.1 在 Windows 95 和 Windows 98 问世后仍然拥有巨大的市场份额这一事实。
5. 为什么人们似乎经常抵制良好的工作实践? 我认为我们经常寻找新功能,因为我们想要一个工具来解决我们的问题,而无需我们改变我们的实践。开发有效的工作实践可能需要很长时间。如果看起来工具的新功能将使我们能够避免采用新实践的努力,那么我们都会支持它。极少数情况下,突破性的功能会提高我们的生产力,但我认为如果我们改进我们的工作实践,回报会更大更好。
人们抵制改变。改变需要脑力劳动。改变需要谦虚。许多人很难做到这些。许多良好的工作实践既简单又显而易见,但我们避免它们是因为我们避免改变。具有讽刺意味的是,采用更好的工作实践可能比学习花哨的新工具更容易,但人们仍然不欢迎这种改变,即使这种改变可能会减少他们的工作量。
然而,随着时间的推移,并且通过正确的展示,人们可以学会使用更好的实践。我就是一个活生生的例子。一旦你克服了看起来很愚蠢的心理障碍,尝试新的实践就会变得容易。
明智地选择你的工具,但在你的员工身上投入比在他们的工具上更多的精力。人是生产力的源泉。
(所提供的某些示例已出于教学目的进行了修改,并且为了保护无辜者和有罪者。)
唐恩·M·西利 是风河系统公司的技术 staff 高级成员,他在该公司从事嵌入式系统技术方面的工作。他是伯克利软件设计公司(4BSD 的第一家商业供应商)的联合创始人。西利撰写了《蠕虫之旅》,这是关于 1988 年莫里斯互联网蠕虫事件的最初论文之一(最初于 1988 年 12 月 10 日在线发布;首次以印刷形式发表于 Usenix 技术会议论文集 [1989 年冬季刊],第 287-3040 页。
最初发表于 Queue 杂志 第 1 卷,第 6 期—
在 数字图书馆 中评论本文