文 / Peter Seibel 译 / 郝培强
本文是Common Lisp专家Peter Seibel对Google公司首席Java架构师Joshua Bloch的访谈,谈到API对设计流程的影响和Google的Java观,以及数学、散文与程序员的关系。
Seibel:你认识有什么伟大的程序员不会数学或者没有接受过良好的数学教育的吗?要成为一个程序员,学习微积分、离散数学和其他的数学知识真的那么重要?还是做程序员只需要一种思想方式,即使没有受过这些数字训练,也能拥有?
Bloch:我觉得是思想方式,学不学数学都能拥有这种思想。但是学一下确实有好处。我曾有个同事叫madbot,Mike McCloskey。他很懂数学,但是没有学过数论。他重写了BigInteger的实现。原来的实现是C语言函数包的封装,他发誓用Java重写,要达到基于C语言版本的速度。后来他做到了。为此他学了大量的数论知识。如果他的数学不行,他肯定搞不定这个项目,而如果他本来就精通数论,就无需费力去学习了。
Seibel:但是,这本来就是个数学问题啊。
Bloch:对,这个例子不恰当。但是,我相信即使是跟数学无关的问题,学习数学培养出的思维方式对编程来说也是必不可少的。例如,归纳证明法和递归编程的关系非常紧密,你不理解其中一个,就不可能真正理解另外一个。你可能不知道术语基本情况和归纳假设,但是如果你不能理解这些概念,你就没有办法写出正确的递归程序。所以,即使是在与数学无关的领域内,不理解这些数学概念的程序员也会遇到很多困难。
你刚才提到了微积分,我觉得它不那么重要。可笑的是这么多年来似乎已经成为了一种思维定势了,只要你受过大学教育,那么人们就认为你应该懂微积分。微积分中有很多美妙的思想,可以让人展开无穷的想象。
但是,你可以以连续或者离散这两种不同的方式思维。我觉得对程序员来说,精通离散思维更为重要。例如我刚提到的归纳证明法。你可以证明一种假设对所有整数都成立。证明过程就像施魔法一样。首先证明它对一个整数成立,然后证明针对这个整数成立意味着针对下一个整数也成立,这样就能证明它适用于全部整数。我认为对程序员来说这比理解极限的概念要重要得多。
好在我们无需选择。大学课程里这两样都教得不少。所以即使你用微积分用得没离散数学那么多,学校里还是会教授微积分的。但是我认为离散的东西比连续的东西更重要。
Seibel:前面你提到写程序和写散文有许多相似之处。尽管数学和计算机、编程的联系一直很紧密,但是不是可以认为,写Web框架或者基于Web框架的Web应用程序所需要的技能跟写作的关系更为紧密呢?
Bloch:是啊。前面你提到Java程序员有两个不同的社群。编写库、编译器和底层框架的社群,更需要数学知识。而如果你是在底层框架之上编写Web应用程序,那么必须了解如何进行沟通,言语上的、视觉上的沟通都需要了解。遇到那些令我操作失误的网站我就很恼火。显然有些人完全没有考虑过用户怎么使用他们的产品。所以实质上,编程能力是一系列不同技能的结合。你擅长哪些技能,决定了你擅长编写什么样的程序。但是,即使是库、编译器以及底层框架也需要代码可读、可维护。如果你不擅长写作,你就很难达到你的目标。
Seibel:你设计软件的流程是什么样的?打开Emacs就开始写代码,然后改来改去直到程序写好?还是坐到沙发上拿着一打纸先列个提纲?
Bloch:很多年前,我在OOPSLA(译者注:面向对象编程、系统、语言和应用国际研讨会。)上作了一个演讲,题目是“如何设计一个好的API,以及这为什么很重要”。网上可以找到这个演讲的几个版本。它很好地解释了我的设计流程。
最重要的是了解你到底要设计什么,也就是你要解决的是什么问题。需求分析的重要性怎么强调也不过分。有人认为:“噢,需求分析呀。跑到顾客那边问问他需要什么。得到客户的答案不就成了嘛。”
事实绝非如此。这不仅是一个协商的过程,而且是一个理解的过程。许多顾客不会告诉你问题,而会告诉你一个解决方案。例如,顾客可能会说:“我需要你给这个系统加上以下17个特性。”那么你必须问:“为什么?你想用这个系统做什么?你期望它怎么发展?”等等。你要来来回回好几次,直到弄明白顾客真正需要软件去做的所有事情。这些就是用例。
这个阶段最重要的事情就是提出好的用例。一旦有了用例,你就有了用来比较所有备选解决方案优劣的基准。你可以花大量的时间去改进用例,因为一旦用例错了,你就彻底失败了,所有后续的流程都会徒劳无功。
我见过这样的事。有人找来一帮聪明人,还没搞清到底要做个什么样的系统,就开工了。辛苦地工作了6个月,写出来247页的系统规范文件。这是最糟糕的情况。因为6个月后他们精确制定出来的系统可能毫无用处。他们往往会说:“我们已经投资了那么多,制定出来规范文件,我们必须把这个系统做出来。”所以他们创造了一个没有任何用处的系统,这个系统也从未投入使用过。多恐怖啊。如果没有用例就做好了软件,那么当你试图做点非常简单的操作时就会发现:“哦,我的天,像选择一个XML文档并打印这么简单的事情,需要这么多的代码啊。”这是很恐怖的。
所以先获取这些用例,然后编写骨架API。骨架API应该很短很短,也就一页纸的内容吧,一般正好是一页,无需非常精确。你要声明包、类和方法,如果还不清楚他们应该是什么样的话,可以放一句话的描述。不过这不是产品发布要求的那种质量文档。
中心思想就是在这个阶段保持敏捷,逐步完善API,使其满足用例,为原始的API添加代码,看是否可以满足需求。真是不可思议,很多事情事后看真是太浅显了,但设计API的时候,甚至是构思用例时,你还是会犯各种错误。用代码实现用例时你会说:“哦,我的天,全都错了。类太多了。这些可以合并,这些需要拆开。”或者类似这样的话。好在API文档只有一页长,改起来也很容易。
你对API越来越有信心,代码也就越写越长。但是,核心原则是,先写使用API的代码,然后再写实现它们的代码。因为,如果实现代码被废弃,之前的工作就都白做了。事实上,应该在给出设计规范前写API的代码,否则你可能把时间浪费在给最后完全不需要的东西设计规范上。这就是我设计软件的方法。
Seibel:设计Java集合类这样的,一个具体的自包含的API,设计规范需要有多具体?
Bloch:我敢说比你想的要粗略多了。任何复杂的编程都需要API设计,因为大程序都需要模块化,你必须设计模块之间的接口。
优秀的程序员把问题分块,孤立地去看他们。这样做的理由有几条。比如,你可能会在无意中创造出好用的、可重用的模块。如果你写一个单一的系统,它越来越大,等你想分块的时候,就无法找到清晰的边界,最后系统就变成了一个无法维护的垃圾。所以我断言,无论你是否把自己看成API设计者,把问题分块都是最好的编程方法。
这就是说,编程的世界非常广阔。如果对你来说编程就是写HTML代码,那么这也许不是最好的编程方法。但是,我认为对于大多数编程来说,这就是最好的方法。
Seibel:所以你希望系统由不同的模块松散地耦合在一起。要达到这样的目标,现在有两种不同的看法。一种是坐下来实现设计模块间的API,像你前面提到的那样。另外一种是,“构建可运行的最简系统,然后毫不留情地重构”。
Bloch:我不认为这两种方法有什么冲突。某种程度上,我谈的就是测试先行编程,以及对API的重构。如何测试你的API呢?在实现API之前编写它的测试用例。虽然我还不能运行用例,但我在进行测试先行的编程:实现用例后看API是否能完成任务,我用这样的方法测试API的质量。
Seibel:也就是说你写好使用API的用户代码,然后评审代码:“这就是我要的代码吗?”
Bloch:对!有时候你都不用走到评审用户代码的这个阶段。写代码的时候可能就会有感悟,“写不出来,我忘了这部分API的功能了。”或者“这代码写起来太乏味了,一定哪里出错了。”
这跟你多么优秀无关。不用API写代码,就不可能看出API有什么问题。设计了一个东西,使用了才知道:“哦,错的这么离谱。”如果这是在你浪费大量时间基于这个API写了无数代码之前的话,那么这就是一个重大的胜利。所以,我谈得更多的是测试先行编程和对API的重构,而不是重构API的实现代码。
说到能够运行的最简程序,我完全赞同这种提法。API设计有一条基本原则:疑则不用。它必须是完全满足你关心的所有用例的最简系统。而不是说“把乱七八糟的代码堆在一起”。有很多格言警句说明了这点。我最喜欢的一条是:“简单没那么容易做到。”坊间认为就是Thelonious Monk说的,实际不是,是误传。
没人喜欢烂软件。人们提倡“构建可运行的最简系统,然后毫不留情地重构”,而不提倡“写垃圾代码”,更不会说“不要做前期设计”。我曾跟Martin Fowler讨论过这个问题。他坚信,只有仔细推敲要做的东西,系统才会有合理的形状和结构。他说过,“不要在写代码前先写下247页的设计规范。”我很赞同。
我不赞同Martin的一点是:我认为测试不能用来取代文档。只要你写了别人编程时可以利用的代码,你就需要做出精确的说明,而测试确保这些代码符合你给出的说明。
所以两大阵营确实有些不同意见,但是我认为他们之间的鸿沟没有某些人想象的那么大。
Seibel:既然你提到了Fowler,咱们就聊聊他。他写了很多关于UML的书,你把UML当设计工具用过吗?
Bloch:没有。我觉得用UML做些图表让其他人理解起来可能更容易。但是说实话,我根本记不住那些组件应该是方的还是圆的。
Google是否可以多用点Java
Seibel:作为Google公司里面的Java程序员,你有没有想过Google是否可以多用点Java?如果不考虑现实因素,假如轻挥一下魔棒就可以把Google所有的C++代码用Java代替,这样行吗?
Bloch:某种程度上是可以的。系统的大部分都可以用Java编写,而且现状,也是逐渐往这个方向发展的。但是对系统的绝对核心,例如索引服务器的内循环来说,性能上的一丁点儿提升都有巨大的价值。当这段代码运行在很多机器上的时候,你让它稍微快一点,那么无论是在经济上,还是从环保角度看,都会获得很大的收益。所以有些代码你恨不得用汇编来写,汇编就比C语言更好吗?
我不是对某件事物特别虔诚的那种人。能用就好。我写了20年的C语言代码。从消耗多少程序员的时间的角度来看,使用更现代的编程语言更有效率,而且更现代化的编程语言更安全、更便利,表达能力更强。在大多数情况下,程序员的时间比计算机的时间更宝贵。但是当你的程序运行在成千上万台机器上的时候,就完全不同了。所以我们写的有些程序,使用那些可能不那么安全的语言,榨出每一点值得榨出的性能。现在程序员们使用的现代语言效率都差不多,如果有人说他们的语言效率高十倍,那么多半是在骗你。
但是从工程师写程序耗时的角度去看,差异很大。首先,更现代的语言已经排除了大量的错误实践。其次,它们包含了大量的工具,可以提高工程师的工作效率。可以说这是一种文化,是人们在学校学的语言。但是它也是工作中的基础工程问题。例如,假如一种语言有宏处理器,那么就很难给它写出好的工具。解析C++比解析Java要难多了。
现在,Google用Java写的代码比以前多多了。我不知道具体的数量,但就算还没有达到临界点,估计也快了。不过,各种语言都有多少行代码和在各种语言下执行多少个循环是有很大区别的。试图把索引服务器的内循环用Java改写很愚蠢,不值得称道。如果你是初创一个公司要做类似的事情,可以用Java或者其他现代的安全的语言来写大部分代码,但是在不需要它们的时候,不要用它们。我们有自己的工程基础架构。代码库、监控工具等所有的东西都维系着它。就算Java最终不能获得同等的地位,也会在这些系统中有很多用处,这就不错。我刚到Google的时候,还不是这样的。
如果很早就着手建立公司的DNA,就能够获得巨大的成功,但是这也令他们很难换掉那些早期应用良好、现在已经过时的技术。我记得1982年左右,我在约克镇高地的IBM研究中心实习的时候,那里的主流还是批处理系统。甚至当他们已经开始做分时系统的时候,他们还用虚拟读卡机(编程卡片)、虚拟打孔器这样的术语交流。什么东西都用80列的记录。而DEC一直将思维禁锢在分时系统上。我估计微软也面对这样的问题,就是他们的思维能否超越桌面PC系统。
Seibel:20年内,人们将会谈论Google为何只能在互联网上卖广告。
Bloch:没错。毕竟,在Google还有一部分人认为Java太慢而且不可靠。有这种看法的原因很显然,那就是1999年左右发布的用于Linux的Blackdown Java(译者注:一个非官方移植的虚拟机),它确实又慢又不可靠。既有的看法总是很顽固的,很难改变。事实上Google在很多核心功能上使用Java,甚至包括广告。
所以某种程度上,他们知道Java既快又可靠。但是在实际的搜索流程中,对机器循环最敏感的领域,所有的东西都基于C++,这么做很明显的一个原因就是公司的“基因”。这将在很长一段时间里影响着我们。
Seibel:你实际编程中用哪些工具?
Bloch:我就知道你迟早要问这个问题,我是老帮菜了,提这个都觉得丢人。Emacs的键盘快捷方式在我的脑子里面已经根深蒂固了。而且我喜欢写小的程序,代码库之类的。所以,我写代码的时候几乎不用现代的工具。但是我知道,很多现代的工具可以提高效率。
写大程序的时候我确实使用IntelliJ,因为我们整个团队都在用,但是我不是这方面的专家。这个工具给我留下了深刻印象,我喜欢这些工具对代码做的静态分析。我找用Eclipse、NetBean以及FindBug的人来帮我审阅《Java解惑》,书中的很多错误陷阱都可以被这些工具自动检测到,太了不起了。
(本文来自《程序员》杂志10年12期,更多精彩内容敬请关注12期杂志)
《程序员》12期精彩内容:十年
《程序员》杂志订阅