第五章 有多少眼球驯服了复杂度

可以很明显地观察到市集模式极大地加速了除错与程序演化。另一件可以清楚明白的是,在微观上,开发者与测试者的每天活动中,市集模式如何与为何可以达到这样的成果。在本章(初版完成三年后,依据开发者多次亲身体验过的洞察力),我们将仔细的检查它的实际机制。非技术性倾向的读者可以略过这一章,直接跳到下一章去。

一个关键点是,为何没有源代码意识的使用者所报告的错不会太有用。没有源代码意识的使用者倾向报告表面上的问题,他们把自己的使用环境视为理所当然,所以他们会忽视重要的背景资料,报告错误时很少会包括可信赖的过程。

这里的问题是测试者与开发者对问题的视角不同,测试者由外向内看,开发者由内向外看。在封闭源代码的体系中,两者只会固守自己的角度谈论事情,因而对另一方深深的失望。

开源的体系则打破这条界线,使测试者与开发者可以站在同样的角度来讨论事情,这有高效多了。实践中,这将有巨大的差异,一者是只报告表象的症状,一者是以开发者那种以源代码为基础的角度来看问题。

大部分的时候,大多数的bug是可以由描述开发层级的特征来除错的,即使是不完整的描述。当一个 beta 测试者告诉你在那一行代码有边界的问题时,或告诉你在 X、Y 跟 Z 的情形下,有个变量有问题,指出有问题的代码通常就足够找出问题并修正它。

因此,对于 beta 测试者与核心开发者来说,有源代码意识的人对于双方都可以强化沟通与合作。换句话说,核心开发者的时间被节省了,即使是在有很多共同开发者的情形下。

另一个开源方式的特征是节省开发者的时间,而这是典型开源项目的沟通结构。上面我使用了「核心开发者」(core developer)这个字来区别项目核心(project core,通常很小;一个开发者是常见的,一到三个开发者则是很典型的)与项目圈(project halo)的 beta 测试者跟贡献者(通常有数百个)。

传统软件开发组织的根本问题是「Brooks法则」︰在落后的项目,增加越多程序员会使得项目更落后。一般的状况下,「Brooks法则」的预测是,随着开发者的人数增加,复杂度与沟通成本随着人数的平方上升,而完成的工作却只成线性上升。

「Brooks法则」依据的经验是,bug会在由不同的人编写的代码的接口上大量出现,而沟通损耗会随着项目参与人数的升高而升高。因此,问题的规模会随着开发者间的沟通路径而呈现平方上升。(精确的说,是 N x (N-1)/2,N 是开发者的数目。)

「Brooks法则」的分析建立在一个隐藏的假设基础上︰项目的沟通结构必须是完全图(complete graph),每个人都可以跟每个人沟通。但是在开源的项目中,开发者在有效平行分割的子项目中彼此很少互动;程序更改与bug报告是透过核心团体来处理的,只有在这样的小团体中,「Brooks法则」的分析才成立1

还有其它原因让源代码层级的bug报告变得有效率。事实是一个错误常常会有许多可能的症状,取决于使用的的使用状况与使用环境。这些错误是一些复杂与微妙的bug(像是内存管理错误或视窗的随意中断),也是最难被发现或靠静态分析来捕捉的,这在长期的开发中造成最多的问题。

当一个测试者发出一个尝试性的源代码层级的多症状bug报告(例如,我看来在第 1250 行代码有个窗口在做讯号处理,或你在哪里把那个缓存清空),可能会给开发者一个关键的线索来发现半打的症状,这些开发者通常因为太靠近底层代码而无法发现这样的问题。在这种案例中,很难找出可从外部看见的不正确动作是从哪个bug引起的,甚至是不可能的 ―― 但是透过经常发布,就不需要知道了。其他的合作者会迅速找出bug是否已被修正。在很多案例中,导致不正常动作的源代码层级bug将被移除,甚至在还没有被报告之前就被移除。

复杂的多症状错误,通常有很多从表面症状来的方式可以找出真正的bug。这种能让测试者与开发者找出问题的方式,可能与开发环境有关,也可能会随着时间而有无法预期的变化。实际上,当测试者或开发者追踪一个症状时,都是在程序空间的一个集合中「半随机」(semi-random)取样。bug越微妙复杂,越难找出相关的样本。

对于简单与容易复现的bug,重点在于「半」(semi)而非在「随机」(random);debug技巧、对程序与架构的熟练都是关键。但对于复杂的bug来说,重点就是「随机」(random),这时人多比人少好 ―― 即使这些少数人是平均技巧较高的。

如果从表面的症状找出bug的难度大,像是一些无法从表面症状预测的,上述的结果会进一步增强。单一的开发者可能会以一个困难的方式来做第一次尝试,但其实也可以从简单的方式达到同样的结果。另一方面,假如很多人随着频繁的版本一起测试,可能就会有一个人可以用最简单的方式找到bug,节省了大量的时间。项目管理者将会发现,随着新版本发布,许多人一起用各种复杂方法追踪同一个bug的时代将会过去,尤其是在众人浪费太多时间之前2

第六章 今花非昨花?

由 Linus 行为的研究中,我们得到了一个能解释他为什么成功的理论,所以我想要在我的新项目(当然不如 Linux 内核程序复杂和雄心勃勃)中来测试这个理论。

但我做的第一件事情是大力重组和简化 popclient 的程序,Carl Harris 的实现非常扎实,可是却像许多的 C 程序员一样,含括了一种不必要的复杂,他以代码为主,数据结构为辅,因此代码看起来漂亮,但数据结构却很特殊,甚至可以说是丑陋的(至少以这位老资格 Lisp 高手的高标准而言)。

然而,我重写程序除了改良原来代码和数据结构的设计外,还有其他目的,就是把它发展到我可以完全了解,否则负责修补你不懂的程序是一件很无趣的事。

项目进行的第一个月,我简单地依循着 Carl 原来基本设计的用意,第一个重大的改变是我加入 IMAP 协议的支持,我重构原来处理协议的程序,改成一个较为通用的驱动程序再加上三个驱动它的方法表(即 POP2,POP3 和 IMAP)。这个改变阐释了一个广义的原则,特别在像 C 这种先天上未提供动态类型的程序语言,程序员们最好谨记在心︰

格言9︰聪明的数据结构配上笨拙的代码要比相反的组合好。3

Brooks 在《人月神话》的第九章中也说︰「光给我看你的代码,而不给我看它用的数据结构,我会一头雾水。给我看你程序的数据结构,我通常不需要再看你的代码,因为已经够明白了4。」

1996 年的九月初,从零开始工作约过了六周,我开始在想是否要帮 popclient 取个新名字,毕竟 popclient 已不仅仅是单纯的 POP 协议客户端程序,但我迟疑了,因为 popclient 的设计并无真正重大的改变,我的 popclient 尚须发展出自己的特色。

当 popclient 可以把 fetchmail 抓下来的信直接转发到 SMTP 的端口时,它彻底的改变了;至于 fetchmail,我稍待会再说明。我之前说过要用这个项目来测试关于 Linus 成功的理论,也许你会问我到底要怎么做呢?我用下面几个办法︰

  • 我尽早并经常发布新版本(几乎至少每十天就发布一次,甚至在发展的高峰期,一天一次)。
  • 对于每一位与我讨论 fetchmail 的人,我把他们列入 beta 测试者的名单,所以名单越来越长。
  • 每当我发展出新版本,一定发出像聊天般的通知给 beta 测试者名单上的人,鼓励他们一起来参与这个项目。
  • 而我也总是倾听 beta 测试者的心声,询问他们对于这个程序的设计上有无意见,并且回应他们送来对程序的修补和反馈。

在采用上述的办法后,立即就得到了回报,自从这个项目开始以来,我所收到关于程序错误的报告,其品质足以令许多的程序开发者羡慕,这些报告甚至还常常附上不错的修补办法。因而我做了关键性的思考,我收到了使用者的来信,得到了关于新增智慧型功能的建议。这说明了:

格言10︰如果你视 beta 版测试者如同你最珍贵的资源,那么他们就会成为最珍贵的资源。

Fetchmail 达到成功的方法中,有趣的是一张薄薄的 beta 版测试者名单,也就是 fetchamil 之友的名单,当我在写这个程序时,有 249 位,然后每周增加 2 到 3 位。

这张名单中的成员人数最多时几乎到达三百,不过,当我在 1997 年五月底审订这张名单时,其中的成员已经因为一个有趣的原因而开始减少,好几位告诉我他们要停止订阅「fetchmail之友」,因为他们觉得 fetchmail 已能满足他们的需求,已经不再需要收到「fetchmail之友」。也许这是成熟的市集模式项目的正常生命周期中的一部份。

第七章 Popclient 变成 Fetchmail

这个项目真正的转捩点发生在 Harry Hochheiser 发给我他写的原型程序,这个程序会把邮件转发到客户端机器上 SMTP 的端口,我立即了解到这个特色若有稳定的实现,那么 fetchmail 中其他的邮件传递模式都可以废除了。

有几个礼拜,其实我一直在扭曲 fetchmail 而不是真的改进它,因为它使用介面的设计虽然能提供服务,但却不够高雅,并且有太多非必要的选项成为整个程序的累赘,尤其是要把取回的邮件存成邮件档或输出至屏幕的选项对我造成了相当的困扰,可是我却也说不出个所以然来。

当我思考邮件改由 SMTP 转发这个做法时,才发觉到原来的 popclient 包揽太多事了,过去它被设计成邮件转发代理(MTA)兼邮件递送代理(MDA),若藉由 SMTP 转发邮件,那它可以完全不管邮件递送,单纯地负责邮件转发,只要把邮件转给像 sendmail 这样的邮件递送程序就可以了。

在有支持 TCP/IP 通讯协议的平台,几乎可以保证第 25 号端口(SMTP用)早就在那里等了,为什么还要和设定邮件递送代理组态或设定邮件档的上锁附加模式这些问题纠缠呢?尤其这样做可以保证取回的信件看起来像发信人透过SMTP传送一样,而这正是我们想要的。

在这里给我们上了好几课,第一课是,这个透过SMTP转发的巧思是从我仿效 Linus 的方法以来,所得到最大的收获,一位使用者提供了绝佳的主意,而我所必须做的已经蕴涵在其中。

格言11︰仅次于拥有好点子的是从你的用户那里识别好点子,有时候后者反而更好。

你将会发现一件很有趣的事︰如果你很诚实并很自谦地知道你欠人多少,那么全世界都会认为你发明了全部,而且对你先提出的天才创作,也会以为你非常谦虚,这些我们可以在 Linus 身上得到印证。

(1997 年 8 月的时候,当我在 Perl 会议上发表这篇论文时,Larry Wall 坐在前排,我念到上一行时,他叫了出来,以一种复兴宗教的神情,喊著︰「兄弟,告诉他们,告诉他们吧!」,全场的听众都笑了,因为他们知道这也发生在这位 Perl 的原创者身上.)

我以同样的精神进行这个项目,经过短短几周的时间,我开始得到类似的赞美,这些赞美不只来自 popclient 的使用者,也来自该得到这种赞美而却未得到的人,我保留了一些感谢函,也许当我怀疑我人生的意义为何时,可以再看看这些信。

但除些之外,这里还有两课更基础,不具政治性,更适合所有设计的一般情形︰

格言12︰通常,最有突破性和最有创意的方案法来自于发觉自己对问题原先的观念是错误的。

我曾试着去解一个错的问题,就是延续 popclient 既是 MTA 又是 MDA 的设计,把它发展成有各种的本地端递送模式。Fetchmail 的设计需要重新思考,应该只要单纯地做一个 MTA 程序,成为因特网正规的 SMTP 邮递路径中的一段。

当你在发展程序的过程中撞到障碍时 ―― 也就是当你发现很难想出下一步要怎么修补时,通常是反省的时候了,但不是问是否已找到正确的答案,而是我们提出正确的问题了吗?也许问题需要再重新整顿一番。

是的,于是我重新整顿了我的问题,很明显地,该做对的事有︰(1)在原来通用的驱动程序中,加入转发邮件至 SMTP 端口的功能。(2)把它作为预设模式。(3)丢弃其他递送模式的代码,尤其是递送至邮件档及递送至标准输出。

我对第(3)步迟疑了一些时候,因为担心会吓走长久以来 popclient 的使用者,因为他们一直倚靠另一种递送机制,理论上他们可以立即以 .forward 档来达到同样的效果而不靠 sendmail 程序,事实上这个转换可能含糊不清。

但当我真的去做,结果证明益处极大,popclient 驱动程序中的一段可以消失了,设计也变得简单多了 ―― 不用再屈就系统的 MTA 程序及使用者的邮件信箱档,也不再需要担心底层的操作系统是否支持文件锁。

而且唯一丢掉邮件的可能也不见了,如果你指定要把邮件送到某个文件而磁盘空间又满了,那么你的邮件就丢掉了,然而由 SMTP 转发信件的话,则不会发生这种事,因为 SMTP 的接收者除非将信息送达,或至少先暂存起来待会再送,才会回复成功给发信者。

并且效能也改进了(如果只跑一次,你大概不会有感觉)。另一个有意义的好处是使用说明变得更简单了。

稍后,为了要处理某些模糊的状况,如动态 SLIP,我必需让使用者可以指定本地端要用哪一个 MDA 程序来送达邮件,我发现了一个更简单的方法。

这寓意是什么呢?当你可以丢掉程序中老旧的特色而又不失掉效力,那就别迟疑。Antoine de Saint-Exupery5(当他还不是经典童书的作者前,他当过飞行员和飞机设计师)曾说︰

格言13︰设计上完美,不是「没有东西能再被加入」,而是「没有东西能再被移出」。

当你觉得做对了,那么你的代码越来越好,越来越简洁,在这个过程中,fetchmail 的设计终于和先前的 popclient 不同了,而有了自己的特点。

该是这个程序改名字的时候了,新的设计看起来比旧的 popclient 更像 sendmail,新的 popclient 和 sendmail 都是 MTA,只是 sendmail 把邮件「推」出去给 SMTP 收信程序,再送达使用者,而新的 popclient 则是把邮件「拉」回来给 SMTP 收信程序,然后再送出,所以两个月后,我把它更名作「fetchmail」。

第八章 Fetchmail 成长了

我把 fetchmail 设计得雅洁而新颖,程序本身也跑得很好,因为我天天都在用,并且开始有一些 beta 版测试者加入,这情形使我逐渐了解,我已不再是为了可能让少数其他人能得到一些便利,而在进行用处不大的程序开发,我已经为每一位有台 UNIX 机器,上面跑 SLIP/PPP 来取得电子邮件的玩家们,写了一个真正满足他们需要的程序。

因为藉 SMTP 端口转发邮件的这个特色,潜在使得 fetchmail 足以成为同领域的杀手级程序,在同类的典型程序中,它已经够资格占到适当的位置,使得其他程序不是被捨弃就是几乎被遗忘。

我认为你不该设定或计划会有这样的结果。你必须有强大的想法并全力投入,以至于之后的结果似乎是无可避免的,自然的,甚至是注定的。要追求像这样好的结果,唯一的方法就是先拥有许多的想法,或者以工程上的判断去取得别人好的想法,而在此处这个想法的利用已超出原创者的想像。

Andrew Tanenbaum 在 386 上造出了一个简单的原生 UNIX 系统,他原先的想法只是用来作为教学的工具,但 Linus Torvalds 把这个 Minix 系统的观念拓展开来,更进一步,已经超过 Andrew 当初能够想像到的发展,并且成长出一些令人赞叹的事物。我用一样的方式(虽然规模较小),由 Carl Harris 和 Herry Hocheiser 那里得到一些想法,然后把它们发扬光大。在人们的想像中,历史上的原创者都是天才,而我们两位都不是,但是大部份的科学发展和软件发展工作,完成者不是天才原创者,反而是行家们。

Linux 和 fetchmail 的成果都是一样令人兴奋,事实上这就是每一位高手追求的成功!这些成果指示我应该把标准设得高一点,把 fetchmail 发展到我所能想像的搞不,我已不只是为自己的需求而写,也为其他人所需要的功能而写,并且还要同时保持程序简单和强健。

第一个加入的重大特色是「集体邮箱」的支持 ―― 这个功能可以抓下累积在同一个群组信箱中,而属于不同使用者的信件,然后再分送给原来的个别收信人。

我决定加入对「集体邮箱」的支持,部分是因为有一些使用者们嚷嚷着他们需要它,但主要是因为我认为它会迫使我以更通用的法则来处理邮件头的地址,并藉此除去「一人一信箱」功能代码中的错误,而我的确也做到了。为了让程序能按 RFC 8226 中的规定来检查信息的语法,花了我相当长的时间,不是因为规定的个别片段难以理解,而是因为它包括了成堆相互依赖的琐碎细节。

结果「集体邮箱」的支持的确是一个漂亮的设计,我是怎么知道的呢?

格言14︰任何的工具以我们所知道的方法来使用都会有用,但一个真正了不起的工具会以你从未想过的使用方法来发挥它的功能。

支持「集体邮箱」的 fetchmail 有一种意料之外的使用方法,就是在以 SLIP/PPP 连线方式连上 ISP 的客户端执行「邮递讨论名单」(mailing list),因为它可以配合客户端的多使用者共用 ISP 上同一信箱(用别名 ―― alias ―― 的方法),让每个使用者都能在名单上,这表示我们可以在个人的电脑上,透过一个 ISP 的帐号,来维护一个邮递讨论名单,而不必持续连著 ISP。

另一个来自我 beta 测试版的使用者的重要需求是接受 8 位 MIME 邮件格式,这很容易做到,因为我过去一直都小心地保持每一个ascii码都是完整的 8 位,并非我未卜先知,而是我遵从另一条法则︰

格言15︰写作任何的网关类软件时,要尽可能地不去扰动到通讯的数据流 ―― 并且绝对不要丢掉其中任何的数据,除非接收方强迫你这么做。

如果我当初没有遵从这项原则,那么 8 位 MIME 的支持势必难以加入并容易出错,所以我需要做的只是研读 RFC 16527,然后加入显然得知的代码以产生 MIME 的标头。

一些欧洲的使用者要我在程序中加入选项,用来限制每次连线取回邮件的数目(这样他们才能控制昂贵的电话线路连线花费),我拒绝了许久,甚至到现在,我对这个选项的加入仍感到不太愉快,但假如你是在为全世界写程序,那么你就应该听取你客户们的意见 ―― 这个原则不会改变,因为他们会以金钱之外的形式给你报酬。


  1. 这不完全精确,因特网为基础的开源项目的混合式组织特征,依 Brooks 的建议,要解决这样的 N^2 复杂度问题,应该採取「外科手术小组」的团队 ―― 但是现实中差异很大。像跟在领导者身边的代码图书馆员这样的专门角色,实际上不存在;相对于 Brooks 当时来说,这角色被更而有力的软件工具集所取代。而且,开源文化强烈依赖 UNIX 传统的模组、API 与数据隐藏 ―― 没有任何一个是 Brooks 所讲的处方。 ↩︎

  2. 反对者跟我说,对同一bug的多症状来说,描述bug特征的难度呈现指数式(譬如 Gaussian 分布或 Poisson 分布)上升。如果可能掌握到分布的外观,那会是很有用的资料。这与debug困难度是均匀分布的差别很大,也就是说单一开发者也应该模仿市集模式,针对单一的bug定一个时间上限,超过的话就追踪下一个bug。从一而终不见得是美德… ↩︎

  3. 相反的组合指笨拙的数据结构配上聪明的代码。 ―― 译注。 ↩︎

  4. 事实上,上述的这段话他是用「流程图」(flowchart)和「表格」(tables)这两个名词,但由于三十年间专业术语/文化的变迁,这些名词的意义几乎是相同的。 ―― 译注。 ↩︎

  5. 「小王子」就是 Antoine de Saint-Exupery 的作品。这句格言也曾在《Modern Operating System》一书中被 Andrew S. Tanenbaum 引用来说明操作系统微核心的设计哲学。 ―― 译注。 ↩︎

  6. RFC 822 是 Standard for the Format of ARPA Internet Text Messages。 ―― 译注。 ↩︎

  7. RFC 1652 是 SMTP Service Extension for 8bit-MIME Transport。 ―― 译注。 ↩︎