Hello World
Spiga

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

2009-09-26 17:44 by 老赵, 13545 visits

一早看怪怪同学评论《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又有什么区别。

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

22 条回复

  1. Grove.Chu
    *.*.*.*
    链接

    Grove.Chu 2009-09-26 18:06:00

    难道还可以抢到沙发?

  2. 560889223
    *.*.*.*
    链接

    560889223 2009-09-26 19:13:00

    老赵我觉得你这次点偏了。
    怪怪是在批评第五章作者在陈述观点时的方式。他应该是认为,第五章的作者对一个在业界中不确定的问题,以一种确定的口吻做出答复。而当这样的文章借助ThoughtWorks公司的名气传播时,会对初学者产生灌输,而让其没有戒备之心。所谓尽信书则不如无书。
    某种程度上,应该也是觉得第五章作者的叙述不够“维基化”、中立化吧。

  3. 老赵
    admin
    链接

    老赵 2009-09-26 19:25:00

    @560889223
    我只是提到他的说法,让大家去看,我没有去“附和”他的说法。
    我全都是在说自己的看法,和他完全无关,呵呵。

  4. johnhax[未注册用户]
    *.*.*.*
    链接

    johnhax[未注册用户] 2009-09-26 20:07:00

    关于groovy和java那个例子,我觉得你的批评有些苛刻。因为我觉得当我们说一个语言时,其实绝大多数时候并不是只是指狭义的,由Spec所定义的语言特性,而是也包含语言的标准类库,及其背后所暗含的风格和习惯。所以诚然你指出的,如果有新设计的类库或者自行做的辅助类,java在这个例子中一样可以做到精炼。但是那其实反过来牺牲了java语言之为工业语言的另一些优势,即标准库和普遍的习惯。当我们批评java生产力低下的时候,其实不是光光批评java语言特性导致的问题,也包括java所建立和java开发者所依赖的风格和习惯,以及从这种风格和习惯进行转换的潜在成本(学习新类库或写辅助类需要成本,也许都大于等于直接学习groovy的成本?即使略小于,但groovy比java还有其他优势)。

  5. 老赵
    admin
    链接

    老赵 2009-09-26 20:12:00

    @johnhax
    嗯嗯,你说的也有一定道理。不过,就我个人而言,我一般倒只是关心语言特性,因为我认为类库是接近“一次性投资”,它的代价在长期的开发过程中会被掩盖,因此不那么重要,所以我评价语言,往往都是看哪些方面是真的无法用类库来代替的。

  6. Shuhari
    *.*.*.*
    链接

    Shuhari 2009-09-26 20:21:00

    你是在用自己的想法去套作者的思路了。作者要表达的意思是,Java对于简单的任务过于繁琐,因而对于日常的工作并不合适。人家并无意区分这个差别是来自语言还是来自类库,不然Groovy的eachLine方法是Java没有的,那这个比较岂不是一开始就不公平?

    其实作者前面也已经说了:Java开发新手要学很多奇怪的内容,而这些内容大多与要解决的问题没有关系,仅仅是为了满足Java中的一些规矩套路。这句话反过来说就是:如果Java设计之初能够不囿于那些变态的规矩套路,那么简化开发是完全可能的。

    此外你写的C#代码和Groovy也不能完全等同,因为为了构成一个可以运行的程序,不论C#还是Java都必须构造一个主类、导入一堆命名空间、编写一个Main入口,而这些工作在Groovy里面是不必要的。

  7. 老赵
    admin
    链接

    老赵 2009-09-26 20:30:00

    我文章里说了,我是同意作者的观点,我也一直认为Java的生产力低下,我只是说这个示例不合适而已。也就是说,你说“作者的意思是XXX”,我是同意他的“意思”的。
    至于说Groovy的eachLine方法Java没有,如果Java补充不了,那么就是公平的。但是现在Java补充的了,因此的确像你说的,我认为的确不公平。
    还有对于新手来说,我想没有比较价值吧,比较的应该是在一个对两种语言都足够熟悉的情况下,哪种语言生产力更高。在熟悉了语言之后,我们平时编程是不会在意那些语言细节的,不是吗?
    至于你说可运行的程序,C#和Java还要补充很多,我就不认同了,因为我认为架子代码本来就是开发工具可以应付的,不是语言生产力的关键。
    平时编程时,我们也不会去编写那些架子代码。不过的确有的问题就不是“架子代码”了,例如一些每行代码都不可以省略的语法噪音等等。
    Java的噪音很大,所以我想一定可以举出更好的例子,原文的问题就是举的例子不好。

  8. Shuhari
    *.*.*.*
    链接

    Shuhari 2009-09-26 21:09:00

    作者也没有说过Java不能增加方法么,你说不公平,是因为你觉得作者在比较语言,但我觉得不是,作者只是在说对于日常工作来说,Groovy比Java方便,就是这么回事。至于说Java补充方法才公平?对实际开发人员来说没有意义呀,人家总不能等你升级版本以后才开始工作吧。

    所以你的比较并没有错,但和作者要说的是不同方面的问题,你不同意作者的论据,可人家的论本来就不是你这个论呀。

    对于结构性代码的问题,本来也不算太大的事情,毕竟多大的程序也只有一个主类。不过敏捷那帮大佬的基本观点是“如无必要,勿增代码”,哪怕很少的结构代码对他们来说也是原则性问题,不能够轻易让步的,这是社区文化的差异,.Net这边绝大多数人并不在乎这个,所以两边经常是格格不入。恰好TW正好是敏捷公司的代表,所以有些看问题的角度和.Net社区不同是意料之中的事情,看看就好吧,理想主义和现实主义的差别,再怎么争论也不会有结果的。

  9. 老赵
    admin
    链接

    老赵 2009-09-26 21:14:00

    @Shuhari
    作者难道不是在比较语言么……日常工作就是项目开发,我觉得通过轻易补充类库就能“弥补”的东西,不算差距。
    还有我认为敏捷和.NET社区是不冲突的,TW那边Java和.NET也有很多,没啥格格不入的。
    至于你说TW把“很少的结构代码”当原则性问题……我觉得TW是务实的,不是理想主义……

  10. Shuhari
    *.*.*.*
    链接

    Shuhari 2009-09-26 21:29:00

    你的观点:能轻易补充类库的,不算差距
    作者观点(当然是我理解的):管你语言还是类库,逼得程序员多写代码就是差距

    所以我一直都说你和作者的观点都没错,但是站的角度本来就不同,结果就是鸡同鸭讲。

    我说的理想主义和现实主义是指在这个具体的设计思想上。敏捷的理想主义是不管代码多少都应当遵循原则,你的现实主义是代码不多,又可以工具生成就无所谓,这是设计理念的问题。在实践的层次上,作为商业公司TW当然是现实的。

    我认为本来这本书的目的应该是关注程序员的效率,并不是为了对语言作一个绝对公平的比较,飞机和汽车的比较有可能公平吗?就算不公平,人家说飞机比汽车快也不能说错么。

  11. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-09-26 22:36:00

    唔,阅读评论下来,感觉“群众纷纷表示”大家关心的是“现成的东西”,而不是“能添加的东西”。语言特性使我们可以添加新的库,但如果没有现成的库那解决现实问题就有眼前马上看得到的麻烦。

    说真的语言背后的库对于语言的应用也确实有很大作用。就像我突然要算一个文件的CRC32时,我会打开Python交互式解释器,从标准库里就有的zlib找出crc32函数,然后打开文件,读,算校验值,完事。而需要算MD5的时候我会转回用Ruby交互式解释器。我想如果Ruby自带的库里很方便的暴露出计算CRC32的方法的话我就会处于偏好而优先用Ruby。

    如果我要用6、7行去手工写个实现Iterable<E>的类来实现无限表,相比之下我更想只用个冒号……||||

    好吧,我没看样章,纯路过……
    P.S. 老赵最近几篇写得似乎都比较急,错别字不少……搜“样张”有2 matches =v=

  12. 老赵
    admin
    链接

    老赵 2009-09-26 22:47:00

    @RednaxelaFX
    是啊,如果要上手作东西,肯定是python,ruby这中比Java要nb几百倍,不过我到讨论的场景是一个长期的项目,至少比临时代码片断要重几百几千倍的东西,所以不在意补充类库。
    话说,我已经把许多样张改成样章了……哎哎。

  13. 王德水
    *.*.*.*
    链接

    王德水 2009-09-27 09:33:00

    这篇文章很中肯,java有自己的特色,java有很多成熟的东西需要我们去学习他的思想。
    python,ruby发展前景很好,但有时我们重复造轮子的时间远远大于java的一些不足所造成的烦恼。

  14. 老赵
    admin
    链接

    老赵 2009-09-27 09:51:00

    @王德水
    我咋觉得这不是我想说的东西……

  15. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-09-27 10:57:00

    Jeffrey Zhao:
    @王德水
    我咋觉得这不是我想说的东西……




    同感。

  16. 王德水
    *.*.*.*
    链接

    王德水 2009-09-27 13:29:00

    @Ivony...
    @Jeffrey Zhao
    后边是我自己的想法

  17. 发条橙子
    *.*.*.*
    链接

    发条橙子 2009-09-27 13:57:00

    老赵,你为什么总在乎别人说什么?你要表达自己的观点一定要先找别人的观点来说明么?搞不懂。

  18. 老赵
    admin
    链接

    老赵 2009-09-27 14:00:00

    @发条橙子
    没错,我要表达自己的观点,需要引用别人的观点作为旁证。
    因为我的目的是科学求证,而不是自说自话,让自己说爽就行了。

  19. Diamond Tin[未注册用户]
    *.*.*.*
    链接

    Diamond Tin[未注册用户] 2009-09-29 11:30:00

    老赵你好:
    你写的很好,不过这里有些问题需要在一些背景下理解。
    1. Neal是一位很NB的讲师,所以它举的例子有些夸张是正确的。因为在小品式的文章中,表达你的立场和观点是最重要的。一些夸张的印象可以帮助读者理解你的意图。如果Groovy和Java的例子都按照本语言中最漂亮的方式改写,那么对比效果就不强烈了。
    2. Neal的这篇文章其实比较老了,这个是在Ruby进入企业开发门槛的时候的一个助推剂。且Neal所用代码片段大都来自实际项目,代码一般不够漂亮,这是他的风格。
    3. 混合语言在Mingle里面非常重要,不是像你所说的不是个好的例子。Mingle显然不是用Jruby运行RoR这么简单,Mingle为了跨平台使用了很多Java类库,通过Jruby调用。Ruby的元编程能力帮助实现Mingle Query Language。Mingle的数据库版本升级靠Rails DB Migrations。消息队列使用ActiveMQ。Mingle有非常完备的自动化测试套件(martin fowler在Ruby五年的Slide中称这几乎是他见到的目前最完备的自动化build)。这样的一个跨平台独立应用绝对是混合语言编程的好例子。

    仅个人观点。

  20. andy hu[未注册用户]
    *.*.*.*
    链接

    andy hu[未注册用户] 2009-09-29 11:50:00

    说的有道理,它们不是语言本质上的问题,Java通过增加类库也可以拥有类似的能力。

    但是,为什么Java不提供这些类库?为什么Java的限制会比较多?

    这可以说不是本质的区别,但这也可以说是本质的区别。Java可以实现,但问题是,它又无法实现成那样。


    ------------------------------------------

    举Mingle的例子,我想作者想表达的是Ruby和Java语言的混用 -- 这可以作为多语言编程的例子。

  21. 老赵
    admin
    链接

    老赵 2009-09-29 11:55:00

    @Diamond Tin
    呵呵,谢谢补充啊。我没有恶意,没有说Neal骗人,只是说例子不妥当,呵呵。
    我说Mingle不合适的原因,它纯粹就是Ruby写的,然后调用Java类库,感觉不是“混合开发”,但是的确是JVM平台下使用Ruby的经典案例。
    我认为混合开发是要利用起语言特性,平台特性。
    例如Facebook,用C++,Java作分布式存储,Erlang做聊天系统,使用PHP粘合起来。
    再比如Twitter,用Ror做Web,但是消息系统是用Scala写的,它和Facebook倒是我认为非常典型的混合编程案例。

  22. 老赵
    admin
    链接

    老赵 2009-09-29 11:56:00

    @andy hu
    我的意思就是,Mingle到底用了Java没?我以为只是用了JVM上的Ruby而已,而类库是已经开发好的二进制码,不是系统的一部分。二进制码,同样什么语言都能编译出来。比如我最近正在设法把Java的一些类库尝试转化成.NET程序集,然后使用,呵呵。

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我