原文:Sick of Ruby, dynamic typing, side effects, and basically object-oriented programming by Abe Voelker
已经有很长一段时间了。我曾打算在新年前后写这篇博客,赶在“Ruby终结”的波澜之前,赶在DHH的“测试驱动开发(TDD)已死”的骚动之前:
我正式地表示彻底厌倦了Ruby,超过了其它—对副作用、不稳定性的厌倦,它们迫使我不得不编写如此多的单元测试。@abevoeker
我暗自想道,越来越多的人知道Ruby开发有问题,有缺陷,但他们仍然在错误的东西上下大手笔。天,你的Ruby app是一坨热腾腾的大便吗?这是因为你编写代码还不够勤奋,或者你没有严格遵照TDD的原则。又或者你对将其分解成正确型的设计类型还不够了解。还可能你没有按照“单一责任原则”、“SOLID”或“Demeter原则”诸如此类。在此,阅读这本关于类型、测试或面向对象设计的书,有所启发后回来找我。
我完全不知道面向对象、或者说尤其是面向对象的设计真正的意义。我却浪费了那么多好时光试图找到它。妈的。@garybernhardt
Ruby应用是技术债务磁体
我曾专职编写Ruby,至今已近三年。我的工作主要包括维护一票编写于2008-2010年期间遗留的Rails2/Ruby1.8.7应用(我上手的时候它们之间实质上是零测试)。即便之后添加了失败测试以修复bug,并尽可能地在加入新特征时多做“TDD”,这些应用还是太讨厌了。不得不担忧因小改动而引发的衰退,这可不是闹着玩的。
就以下体会,我相信不止我一个人:
继承五年以上Rails代码基的开发者会默默盼望公司倒闭 @HackerNewsOnion
这是怎么造成的,更重要的是,怎样防止这种情况的发生?测试本该给我们重建信心,不是吗?那么这种情况就必须编写更多测试、严格按TDD进行吗?
就在它即将发动git blame,而我就要对我维护的这些缺乏测试代码的编写者挥拳的同时,我意识到,这不全是他们的错。更多的测试会使我的处境更痛苦(而且说真的,零测试根本没有狡辩机会),我不信更多的测试就是解决Ruby灾难的万灵药。
测试很难
Ruby中的测试需要花费大把精力,却似乎并不讨好。如果你正好想在编写代码之前想更了解Ruby/面向对象测试,你很快就深陷泥潭了。那些mock、stub、double、fake和spy—要应用各种抽象概念,吸收各种知识。面向对象测试的外围是整个行业,而有一些人毕生都在为之布道。
理论的复杂性导致了安装启用的复杂性。尽管我倾向于RSpec,而其他人使用Test::Unit或MiniTest,所以如果我想在项目(仅仅是单元测试框架)之间移动,就也必须了解那些句法结构。而你往往容易受灵光一现的影响—当前存在许多用fixture代替factory的争论(比如Factory Girl),不过即使在同一实验室的应用中,也存在差异—有专门用于最佳实践的网站,不同版本间也有巨大的不同(比如RSpec从should句法结构变成expect句法结构)。
基本上来说,我想表达的是,Ruby测试既需要大量前期与持续的脑力投资,信息又不够轻便,且测试代码受限于剩余代码的位衰减。这种负担带来的焦虑最终往往使开发者无法编写那些以后可能会使他们有负罪感的代码:
Test-first基础主义就如同禁欲广告:自我厌弃的不现实、无效果的道德斗争。
David Heinemeier Hansson(DHH)--TDD已死。长寿的测试。
依靠开发者承受控制Ruby动态类型负担实在不现实,每当测试时它就像愚蠢又灵活的墙泥。就像必须依靠不得不在每次拿出电锯肢解时重造防护衣服的人。就这一点而言,不论因为何种原因(比如老板要求今天完成,或这是生产突发情况,或这只是脚本等等),你只想说“去你的”,在没有防护的情况下冒着失去手指的危险进行工作。当然在应用于Ruby代码的模拟问题上,你也在迫使其他使用你代码的人承担同样的风险,且风险随着时间推移越来越严重。
测试有必要,但仅作测试还不够
程序测试可用来显示bug的存在,但永远无法显示其不存在
Edsger Dijkstra-- EWD249 《结构编程备忘录/Notes on Structured Programming》
遗憾的是,就算你要达到面向对象TDD专家地位,追求你的planarform,总能写出覆盖各种有效测试的完美代码,你的知识也拯救不了你:
Ruby:它是require搭载着stdlib的模块时,会对整数除法计算产生影响的语言。@tomdale
就算是面向对象TDD的大神也无法写出覆盖所有副作用(像这样改变你的代码)序列的文本。当根据之前已精确运行过的代码得出完全不同的结果时,很难将其归咎于代码。程序把我吓尿的时候我不得不坐等程序运行,下一步会发生什么只能靠猜。
同时我们也必须意识到这些测试试图给骨骼进行补丁的疯狂。我指的是,如果你的银行使用这种软件你会怎么想?你要是感到不舒服,为什么你还一直用它呢?你的商务逻辑这样就可以 了么?
我们真正需要的是我们编写的(更重要的是包括导入其他傻X写的)每条代码有更明确的保证,而不是Ruby所能提供的。我们希望防护坚定不移,而未正确构建的代码不要编译。
-“你竟然吃着午饭,喝着啤酒?”
-“没关系,我用的是安全型语言。”@couch
最可维护的Ruby代码暗示了正确的路径
我看到最整洁的Ruby应用倾向于:
- 将函数分解成一组小的对象
- 尽可能使用不变对象(例如对原函数或刚性函数使用thin veneers)
- 将商务逻辑分离成函数集合,作用于上述所指对象(服务对象)
- 尽量将可变对象和副作用缩减到尽可能少
- 用单元测试(模仿静态类型系统)完整记录和预期对象实例和调用的类型冲突
对我而言,这种代码开始展现函数式程序设计(functional programming language)语言的几种特征。
你去除了可变对象,面向对象就消失了,这正是过去五年中我所发现的。(假设你没有用继承)@garybernhardt
痴迷于Haskell
我决定开始学习Haskell(用Chris Allen’s guide),这是一种语言,我觉得用它能够解决许多用Ruby产生的问题。我说我正在痴迷于Haskell是因为我仅仅只是抓到了它的表面,而尽管到目前为止我真的喜欢我学到的,我还没有在实践中大面积地应用,而且我也没有足够的数学/范畴理论以在理论高度上进行表达。但我读了许多聪明人的评论。
我喜欢Haskell的几个地方:
- 没有可变对象 - 所有数据结构都不可变。
- 没有副作用 – 当调取某人的代码时,他不必担心“发射导弹”。
- 纯函数和指针 – 调出同一个函数和自变量总会得到相同的输出,又不会导致副作用。可以“equational reasoning”。
- 静态型 – 输入错误的程序甚至无法编译。而该型系统强大,能推理类型 – 无需像Java一样麻烦。
- 因为前面提到的纯函数,所以并行和平行方便,副作用低,稳定性高。
- 型会成文件,而且编译器会检查。
- 只要能编译,它往往就能运行。
- 由于以范畴理论为基础,大量代码可重用。在我看来这是面向对象(OO)未达成的承诺。
我想,周围有足量的学习材料,而有了像Chris Allen(@bitemyapp)这样积极的老师,即使是像我这样的傻子/非数学强人也能利用Haskell做重大的工作。Haskell有足够的深度,让我投入越多时间和数学理论(范畴理论)去支撑它,就会打开新的数学知识,我就能非常直接地运用(比如lense和arrow)。也许某一天,我也能像那些数学牛人一样,能用更强大的类型系统表达语言,例如Idris、Agda或 Coq(只要你能想到的)有dependent type的语言。总的说来,Haskell不是一种一接触就很快感到厌烦的语言。
没有万灵药,也不是Ruby不好
我不认为Haskell是完美语言(也不认为现存这样的语言)。而尽管我看似对写测试唧唧歪歪,但Haskell并没有排除对它们的需求。与像动态型语言的Ruby不同,它只是减少了许多不必要的部分。只想让在过去我讲这些时,对我翻白眼的人们清楚这一点。
我也不是说Ruby真的消亡或正在消亡,它没有很快消失。Ruby语句可读性强,容易掌握的语言设计,世上还有一大波gem,可以做很多事情。不论好坏我期待它在未来吸引更多新的程序员。不过最重要的是,可预见的未来中我将继续靠它谋生。
但就我个人而言,当我连续发现比Ruby强大得多的语言时,很难再享受着使用Ruby了。
本文经原作者许可翻译,未经许可禁止转载