代码改变世界

谈谈我对《ThoughtWorks文集》中多语言开发部分的看法

2009-09-26 17:44 Jeffrey Zhao 阅读(...) 评论(...) 编辑 收藏

一早看怪怪同学评论《ThoughtWorks文集》公开的样章,一谈多语言开发(第5章),二谈测试(第13章)。怪怪同学的看法是贬前者而捧后者,并提出“同样一个包装下、同一个公司不同的作者,差异如此之大,那么在我们的学习过程中,就要注意去芜存菁了”。说实话,我没有理解他对第5章的评价,如在“抽象方式”方面的说法我没有太深的理解。如果怪怪看到我这篇文章能够再详细说说抽象方法的看法就再好不过了——目前我只能说我同意他的论点(讨论软件思想学习这方面),但是没有理解他的论据,呵呵。

我对第5章有自己的看法,讲的是多语言开发。我是非常提倡多语言开发的,原因可能一是因为我是语言爱好者,二是因为.NET平台是多语言共存的一个良好环境,我以前也经常提出过用IronPython作动态逻辑的“宿主”,用F#作并行开发等等。因此,我是拥护“混合开发”的一个人。但是,很巧,对于这第5章,我还是同意它的论点(或描述),而不同意它的论据(即示例)。

例如,书中举的第一个例子是使用Groovy的方式读取文打印文件每一行,并在每行之前加上行号:

def number = 0
new File(args[0]).eachLine { line ->
    number++
    println "$number: $line"
}

与书中举出的二十多行Java代码(样章的代码是图片,我不想敲一遍了)相比,Groovy的确是简单了很多。但是在我看来,这个例子是很不妥当的。如果您看一下书中的Java代码就会发现,复杂的Java版本是在于使用了类似C#中StreamReader的做法,以及一个古怪而复杂的LineNumberReader,需要打开文件,再一行一行地读,并且加上异常处理。为什么不使用一个number变量,而且Groovy代码的异常处理在哪里呢?也就是说,Groovy版本并没有完成Java版本所处理的所有问题。

当然,有人可能会说,我们目标是完成工作,本就不需要关心异常,而Java中的异常处理代码是因为语言特性,迫使我们必须这么做。但问题就在于,这其实涉及的是“类库”方面的内容。例如,您看这段C#代码:

int number = 0;
foreach (string line in File.ReadAllLines(@"C:\test.txt"))
{
    number++;
    Console.WriteLine(number + " " + line);
}

这和那段Groovy代码有本质区别吗?但是,在这里代码里有没有使用Java所无法实现特性呢?没有。也就是说,Java代码麻烦是麻烦在“缺少类库”,而不是“语言特性”。因此在我看来,用这个例子证明Java是种生产力低下的语言是不恰当的——远不如我之前谈委托时的示例有说服力。

样章中还举了其他一些例子,如判断是个字符串是否为空所使用的isBlank方法,Java需要写在一个额外的StringUtils类中,而Ruby直接打开String类型并添加新方法就可以了:

class String
  def blank?
    empty? || strip.empty?
  end
end

这个示例与Jaskell(JVM上的Haskell语言)中SafeArray的示例相对就有说服力多了——但是它后面又举了一例:Haskell的函数惰性求职特性可以轻易生成无限长的列表:

makeList = 1 : makeList

这点在Java语言中就不可以了吗(您可以用C#语言想一下可以怎么编写一个辅助方法来实现这个功能——但不要使用yield)?我在读这些文字的时候会有一种感觉,作者是一个动态语言的爱好者,但是在举这些示例的时候并没有想过这些示例的说服力如何,是否真的可以体现出与Java的差距,这差距究竟是语言上的还是类库上的。

而在下一个使用Ruby进行单元测试的示例中,我脑子里差点就冒出“骗子”两个字。为什么这么说呢?首先,您可以去看看样章里Java和Ruby两个语言的测试代码。如果您熟悉单元测试,如果你可以区分Mock和Stub两个概念的区别,您应该也可以看出其中的问题来。

简单的说,Java代码的单元测试使用的是Mock,使用了非常接近于Record/Playback的方式,而Ruby代码使用的是Stub,是单元测试的AAA(Arrange,Act,Assert)模式。Record/Playback是早些年较为流行的测试方式,它首先通过Mock对象“录制”待测试对象的行为,然后交给待测试对象进行测试,最后验证它和“录制”的结果是否相同。这种做法本身就较为复杂,因此目前在很多情况下已经被AAA给替代了。从名字上便可看出,AAA的做法是先安排,再行动,最后验证。它关心的只是被模拟对象“在某些调用时的反应”,而并不在意被模拟对象的整体行为。打个比方,在样章中举的Java示例,其中有这样的代码:

warehouseMock.expects(once()).method("remove")
    .with(eq(TALISKER, eq(50))
    .after("hasInverntory");

看到这个after语句了吗?这个after表明remove方法的执行需要在hasInverntory方法调用之后执行。这就是Record/Playback的特征之一:“录制”的行为是可以要求严格按照顺序的。而AAA模式只关心待模拟的对象在某些调用时的反应状况(所以叫做Stub),因此它在“顺序严格”的情况下反而会麻烦一些。例如,如果您要确保一个ITransaction对象的Commit方法必须在Begin之后调用,使用AAA的方式,可能就要自己准备一个order变量,在Begin和Commit方法中引发回调,并检查order的当前数值了。

在来看看Ruby的代码,它使用的便是Stub,并且——它并没有去确认remove方法和hasInverntory方法的调用顺序。如果要确认的话,使用的代码便会复杂一些了。也就是说,Ruby版本使用的本身就是简单的AAA模式,且功能实现的并不如Java版本的完整。以此说明Ruby用于测试更加方便,是不是有点“忽悠”的嫌疑呢?

顺便一提,大名鼎鼎的Oren Eini正打算Rhion Mocks 4.0的开发,计划之一便是移除Record/Playback模式的支持,仅保留AAA方式。而后起之秀Moq框架从一开始就只支持AAA方式。

上周我读了样章的作者Neal Ford的另一本书《卓有成效的程序员》(一本200多页的小册子),其中“元编程”一章中他也提到了使用Groovy进行单元测试比Java方便很多,在我看来这同样也是类库的问题。因为Groovy可以使用普通方法调用的方式去访问一个对象的private成员,而Java中使用反射会麻烦很多(和C#差不多,因此您可以想象一下)。但是,这完全也可以通过补充一些辅助方法来完成工作。此外,Java代码中最麻烦的还是checked exception特性,Neal使用的Java代码中大部分还是在try...catch。

《卓》是好书,但是Neal在两本书中对多语言编程的论述的确不能让我感到满意。可能混合编程是个大话题,不是一句两句话能说清的吧。

最后,样章提出ThoughtWorks的第一个商业产品Mingle使用了JRuby进行开发,但我认为这不是混合编程的优秀示例。因为——它还是只用了Ruby,即使是运行在JVM上,Ruby语言还是Ruby语言。因此Mingle的成功可以证明JVM上运行Ruby的可靠性,可以证明Ruby的生产力比Java高,但我认为它不能作为混合编程的典型案例。

我想讲的东西讲完了,其实挺矛盾的。一则是我在为自己一直鄙视的Java说好话,二是我“反对”了别人对混合编程的论证,而混合编程也是我一直提倡的东西。不过,毕竟要达成目的也必须通过正确的方式。对就是对,错就是错,虽然我坚持技术人员的信仰,但我也认为个人情感不应该左右判断。如果我看到鄙视Java的说法就叫好,那和某些人无端对微软技术搞FUD又有什么区别。