什么?这不是 Ruby 的最吸引人的三个特性吗,怎么成了毒瘤?
Rails 项目里,一般除了使用 Ruby 的 Enumerable 会使用到 block,除此之外,需要自己写一个带 block 的方法的场景不多。lambda 和 proc 也一样,因为在 Rails 里 还是以 OO 风格为主,MVC 各个部分被抽象成 class 和 method,抽象的好不好是另外一个话题。不过最近大开眼界,发现一种在 Rails 里一路飙 proc 的写法:
class A
def a
d = proc{xxxx}
b = proc{xx}
c = proc{d.call;xxx}
b.call
c.call
end
end
上面代码翻译成 OO 代码是这样子的:
class A
def a
b
c
end
def b
end
def c
d
end
def d
end
end
有什么区别么?似乎没什么?proc 在抽重复代码的功能上和 method 是等价的,不过由于 proc 在 ruby 里是二等公民,在没有独立的寄存空间,要相互调用只能写在一个方法里。抽取方法的作用不止是去重,另一个作用是分离职责。a、b、c 方法都是最小的单元,有着自己的作用空间(class A 内),可以独立运行和测试。
总之,在 Rails 里这样用 proc 给人一种面向过程 + 函数式 + 面向对象合体的感觉...
Rails 项目里元编程和 monkey patch 的应用场景主要是在外部
plugin 不提供相应API接口
的情况下去修复 plugin 的行为或 bug。最近仍然大开眼界,看代码:
ActionView::Helpers::DateHelper.class_eval do
def time_transform_in_words(time)
time.strftime("%m-%d-%Y %H:%M")
end
end
这本来是写一个普通 helper 就可以解决的问题,非要用 monkey patch。一定会有同学会说,“It's no big deal.”,这不影响什么啊。其实反映的是一个人解决问题的思维习惯。举例,一个人想去邻居家做客,从墙就翻进去了,邻居问他“为啥翻墙啊?不会从门进吗?”。答曰“习惯了,都可以进去啦。”其实他连门在哪都不知道,也不想知道。
习惯用 monkey patch 方式解决问题的人就是上面的思路,细思极恐啊。
有时是 model 方法已经比较多了,拆成单独的 method 会污染 class 或者要抽太多参数,拆出 class 改起来又切来切去的不方便,直接放个 proc / lambda 就好了... 不过多是因为偷懒
#14 楼 @hooopo 我给个例子吧,首先不要把 proc 或者 lambda 看成方法 (虽然 proc, lambda 可以像方法一样调用),proc 和 lambda 是一种数据,容易理解的场景是构造一个方法,调用此方法返回一个 proc 或者 lambda, 然后再将这个返回的 proc 或者 lambda 作为参数传给其他方法使用。
我有一个 item_list = [item1, item2, item3, item4, ....], item1, item2, ... 都是 Item 类的实例,Item 有 a1, a2, a3, a4, a5 5 个属性,我现在需要统计 item_list 中 a1 的和,a2 的和,a3 的和,a4 的和,a5 的和,常规做法是,
item_list.reduce(0) {|m, item| m + item.a1.to_i}
item_list.reduce(0) {|m, item| m + item.a2.to_i }
item_list.reduce(0) {|m, item| m + item.a3.to_i }
item_list.reduce(0) {|m, item| m + item.a4.to_i }
item_list.reduce(0) {|m, item| m + item.a5.to_i }
如果运用 lambda, 我们可以这样做,
def build_calculator(item, attr)
->(m, item){ m + item.send(attr).to_i }
end
item_list.reduce(0, &build_calculator(item, :a1))
item_list.reduce(0, &build_calculator(item, :a2))
item_list.reduce(0, &build_calculator(item, :a3))
item_list.reduce(0, &build_calculator(item, :a4))
item_list.reduce(0, &build_calculator(item, :a5))
我这个例子比较简单,体现不出 lambda 的优势,如果 reduce 的过程比较复杂,使用 lambda 就可能有优势了。
写得不好,什么都可能成为毒瘤 任何技术都不是毒瘤,是毒瘤的是滥用技术的人 什么技术用得好,你都会觉得,卧糟,原来还可以这么用 用得不好,你会觉得,卧糟,这什么乱 78 糟的
你这段代码能简化主要上 send 的作用,proc 的用处不明显。改成方法应该是这个样子的,有一点啰嗦:
def add_by_attr(total, item, attr)
total + item.send(attr).to_i
end
item_list.resuce(0) { |total, item| add_by_attr(total, item, :a1) }
item_list.resuce(0) { |total, item| add_by_attr(total, item, :a2) }
item_list.resuce(0) { |total, item| add_by_attr(total, item, :a3) }
item_list.resuce(0) { |total, item| add_by_attr(total, item, :a4) }
def build_calculator(attr)
->(m, item){ m + item.send(attr).to_i }
end
item_list.reduce(0, &build_calculator(:a1))
item_list.reduce(0, &build_calculator(:a2))
item_list.reduce(0, &build_calculator(:a3))
item_list.reduce(0, &build_calculator(:a4))
item_list.reduce(0, &build_calculator(:a5))
这样就正确了,并且简洁了许多
#11 楼 @kayakjiang 我也一直认为,代码写不好的就别再写测试添乱了,很多人似乎都没搞清楚问题在哪。
我说的是测试代码,不仅仅指 TDD。首先代码混乱本身就容易出问题,说明程序员对编程语言都还没有掌握,根本没到用自动化的测试代码来保障质量的时候,遇到的许多问题根本不是加测试能够解决的。这时候硬上测试,不但没有效果,连测试代码都会写得跟产品代码一样糟,而且往往比产品代码还更糟,只能是火上浇油。
最后还是忍不住要说:可惜这种情况是大多数。
#35 楼 @emanon TDD 滥用,就像一个有点犯二的强迫症患者,每次出门都特意去检查房间的灯是否关了,当然家里没有人,灯是需要关闭的,但是这种检查应该是下意识的动作,不应该耗费大量的精力去特意检查,我们需要培养的是随手关灯的习惯,而不是这种变态的检查,退一步讲,即使哪天回到家,发现灯没有关,这也不是一件大不了的事情,无非是浪费了些电,减少了些灯泡的寿命。
编写代码很多时候讲究的是一气呵成,TDD 那种写点代码,测试一下,写点代码,测试一下的节奏实在是像一群讨厌的苍蝇在你的耳朵边不停地嗡来嗡去,很痛苦的。
我曾经参与过一个 rails 项目的开发,此项目的测试覆盖率达到了惊人的 99.99999%, 测试代码的规模和复杂程度已经远远超过项目本身了,有时候花了 10 分钟改个代码,却需要花一个小时甚至更长的时间写测试,更气人的是你还必须写测试。让人啼笑皆非的是有次我发现项目中有段代码没有用处,然后把它删除了,然后测试时一片红,后来我跟同事确认这段代码在项目中确实没有什么用,但是测试需要它,更恐怖的是这些测试代码是耦合的,所以为了测试通过,这段代码一直保留着。
现在很多用 rails 的公司要求程序员 TDD,这实在是一个不明智的要求。
#37 楼 @kayakjiang 看到你对测试见解,忍不住也想说两句。
生产代码中含有协助测试通过的代码,这确实是啼笑皆非,很有可能是开发人员对测试开发认识不够,经验不够导致的,这个栗子不能说测试是原罪,就好像说刀能杀人,就说刀很邪恶一样,没有因果关系的推论,关键是看握刀的人。测试代码耦合这个问题,也是因为经验和认识不足导致的。
The art of Unit Testing 里面提到写测试的目的和原则,以及想要写好测试(每个测试独立运行,互相不干扰,不耦合)是不简单的。你碰到测试经历,正好都是书写测试时候应该要避免的反例。
在这里我避免使用 TDD 这个名词,因为它被理解为“严格的要先写测试,再写代码,其他的做法都是悖逆”。其实真正的测试比这样要广义,同时着力点不一样,测试的意义不在于要先写测试,而在于自动化 (automatic), 反复进行 (regression tests),重构 (refactoring) 和活文档 (live documents)。而测试也不是 rails 开发的专利。js 开发使用 jasmine, C# 使用 unit,我没有考究,但是估计其他语言也有测试框架。不是 rails 开发在写测试,而是做开发的会写测试。据我所知,google 的测试氛围是很浓厚的,他们很注重自动化的测试,参与开发了很多很棒的测试框架,如 seleniums。靠谱的开源项目也需要贡献者写测试,并不是没有原因的。
除此之外,测试是能推进自动化部署(CI[continuous integration], CD[continuous deployment]) 的必要条件,这里我就不多说了,有兴趣可以了解持续集成,持续部署的相关文献。
其实我很希望社区里多一些写测试的人,所以忍不住啰嗦多了几句。:)
元编程是一种抽象的手段,但大多 Rails 项目是针对业务的,而业务本身和业务内部很难有清晰,稳定的抽象界限。稳定的抽象往往是业务面对系统;业务面对网络;业务面对第三方软件库等;而在这些方面的元编程一般已经有人封装好成 gem 了
#38 楼 @yue 你说的不错,写测试代码确实是一件好事,甚至可以说是一件政治正确的事情,但是好人也经常干坏事,写测试代码就如同一个经常干坏事的好人,虽然它的本意是想做些好事情,但是很多不好事情的事情确实是发生在测试代码中,我补充一些我的观点,下面这些场景以我为主角,
片面追求测试覆盖率,导致写了很多类似于 assert 1==1 之类的测试代码;
大量的使用 mock, stub 之类的东西 (很多时候为了解耦测试代码,又不得不用这些东西),虽然测试通过了,但是上线后,系统被残酷的线上环境虐成渣渣;
自认为自己写的测试代码很清晰,可以当作文档,然后就真的不写文档了,但是过一段时间后,发现读测试代码比读源码还难;
写了些测试代码后,盲目地自信,整天惦记着重构的事,当真的要重构时,却发现项目代码和测试代码要一块重构,每天改测试代码改的要死;
写了些测试代码后,盲目地自信,认为打造一个 自动化 (automatic), 反复进行 (regression tests) 的测试系统很简单,并且幻想这个自动化测试系统会很可靠,于是每次通过 CI 成功提交代码后,都会自欺欺人地说:妥妥的。然后第二天被老板骂的找不到北。 打造一个可靠的自动化测试系统,需要一个研发团队持续不断的努力,需要大量的工具,流程和文档,而不只是一些测试代码。
个人体会:
#47 楼 @fleuria 你说了个人体会,这点我是赞同的,关于测试这块的一些评价我也是出于自己的个人经验有感而发的,我其实有一段非常长的写测试的经历,我对写测试的熟练程度可能比这个坛子里大多数人都要多些,并且我现在仍然会对系统的一些比较重要,比较核心的地方写测试。你说的覆盖到好过没覆盖到
, 差的测试好过没有测试
等等这些我认为是写测试的忌讳,写测试是一件很耗费精力的事情,这个精力既包括脑力,也包括体力,程序员的精力既是有限的,也是宝贵的,把程序员的这些宝贵精力消耗在差的测试上,或者是一些并不是很重要的代码的覆盖上岂不是一种严重浪费,当然如果你觉得自己精力好,或者觉得项目的每一个地方都很重要,都不能有任何闪失,你可以继续这样做。以我的个人经验做个小结:如果决定写测试,那务必就写出质量好的,能够用在刀刃上的测试代码,如果做不到,还不如前期认认真真思考,做好全面的设计,忘记重构,忘记以后再去改善代码质量等等之类将来的事情,并且索性大方点把测试的任务交给 QA 团队去做。