Rails Rails 项目里的三大毒瘤

hooopo · 2015年06月20日 · 最后由 dongli1985 回复于 2015年07月17日 · 9669 次阅读

Rails 项目里的三大毒瘤

  • (滥用)proc/block/lambda
  • (滥用)元编程
  • (滥用)monkey patch

什么?这不是 Ruby 的最吸引人的三个特性吗,怎么成了毒瘤?

(滥用)proc/block/lambda

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 给人一种面向过程 + 函数式 + 面向对象合体的感觉...

(滥用)元编程 和 monkey patch

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 方式解决问题的人就是上面的思路,细思极恐啊。

话说我的 Ruby 水平还完全不会上面这 3 个用法... 第 3 个平时也就写个 helper 然后在 view 里面调用也就行了,就像虎叔说的

如果不是做框架,这些东西最好干脆不用

#2 楼 @tini8 这是另一个极端了,功能和特性应该使用在最合适的场景,是一个 Trade-off 的过程。盲目肯定和否定都不是我的意图。

大家的编程功底好深,proc/block/lambda 至今没有用到。

:plus1: 这个必须顶,很多人是为了元编程而去元编程的。

#3 楼 @hooopo I couldn't agree more.

在项目里面尝试用过用元编程,但最后都被我删掉了(除了一次,确实解决了很大的问题),因为觉得带来的麻烦远远大于带来的便利。

遇到问题一般情况下都不会想到去用这些方法,一般普通方法就搞定了。

上次在 V2EX 友善建议一个朋友看完元编程后就忘了元编程这事,被好几位喷初学者呢哈哈哈。

知道元编程 -> 知道怎么用元编程 -> 知道什么时候用元编程。

这三个还算不上毒瘤,在 Rails 社区中奉为圣旨的 TDD, 滥竽充数的测试才是大毒瘤

@hooopo 严重同意

#10 楼 @rei 可否分享,什么时候用元编程

哈哈,想看元编程的实际用途?改天写一篇给你们咯…… Just for fun :)

#12 楼 @robertyu 其实这个帖子的另一个目的是希望大家拿出一些实际工作中非元编程 (包括 proc、monkey patch) 解决不了(或难以解决)的问题。

有时是 model 方法已经比较多了, 拆成单独的 method 会污染 class 或者要抽太多参数, 拆出 class 改起来又切来切去的不方便, 直接放个 proc / lambda 就好了... 不过多是因为偷懒

#15 楼 @luikore 污染如何理解?感觉是 private method 做的事情?

#11 楼 @kayakjiang 说说看,如何测试?

#16 楼 @hooopo private 是 private to object 而不是 private to method, 其他方法都能访问它... 而且也可能和 include 进来的其他 module 上的 method 同名... 如果是 local scope 的话, 就那几个局部变量, 名字是超级好想

19楼 已删除

#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 就可能有优势了。

#17 楼 @fresh_fish 像 QA 一样测试 😄

#12 楼 @robertyu 能缩减代码并且提高可维护性的时候。

@robertyu @hooopo ORM 层适合使用 元编程

@robertyu @hooopo 另外注意,我说的是 ORM 层 不是 Model 层, 区别就是 ORM 层属于是通用的组件,而 Model 层是和具体的业务相关。

写得不好,什么都可能成为毒瘤 任何技术都不是毒瘤,是毒瘤的是滥用技术的人 什么技术用得好,你都会觉得,卧糟,原来还可以这么用 用得不好,你会觉得,卧糟,这什么乱 78 糟的

#24 楼 @ery

其实已经我已经写的很清楚了,把讨论范围限定在 Rails 项目滥用 上。

语言层面、框架/工具层面、应用层面是会有不同的考量。即使上像 Rails 框架这样的应用,后面的版本也在慢慢移除一些滥用元编程的做法。

#26 楼 @azhao

您说的这些都正确,但无意义,统称 “正确的废话”。好的用法或不好的用法我想看到拿出实例来讨论,否则就成了嘴炮贴了。

#20 楼 @kayakjiang

你这段代码能简化主要上 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) }

@hooopo 我喜欢你的写法,明显清楚多了。

#29 楼 @hooopo 如果一个 lambda capture 6 个以上的局部变量, 这个方法就很恶心了... 然后 OO 哲学家就会重构出一个 class ...

#29 楼 @hooopo 我的代码有问题,需要改下,

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))

这样就正确了,并且简洁了许多

看到标题第一反应是 simple form, cancan(can), devise…

#11 楼 @kayakjiang 我也一直认为,代码写不好的就别再写测试添乱了,很多人似乎都没搞清楚问题在哪。

我说的是测试代码,不仅仅指 TDD。首先代码混乱本身就容易出问题,说明程序员对编程语言都还没有掌握,根本没到用自动化的测试代码来保障质量的时候,遇到的许多问题根本不是加测试能够解决的。这时候硬上测试,不但没有效果,连测试代码都会写得跟产品代码一样糟,而且往往比产品代码还更糟,只能是火上浇油。

最后还是忍不住要说:可惜这种情况是大多数。

#31 楼 @luikore 我是 lambda 懒鬼,拆一堆方法感觉阅读起来跳跃性太大,偶尔就啪啪啪来个 lambda 省点事哈哈哈

元编程是一种抽象的手段, 但大多 Rails 项目是针对业务的,而业务本身和业务内部很难有清晰,稳定的抽象界限。稳定的抽象往往是业务面对系统; 业务面对网络;业务面对第三方软件库等; 而在这些方面的元编程一般已经有人封装好成 gem 了

#38 楼 @yue 你真的是你头像的人么。。。话说你个人简介里的 link 失效啦 XD

#40 楼 @zzz6519003 八成是抠脚大汉

#40 楼 @zzz6519003 没有失效呀。是本人哟,不过 45 度角,你懂的 :)

#38 楼 @yue 你说的不错,写测试代码确实是一件好事,甚至可以说是一件政治正确的事情,但是好人也经常干坏事,写测试代码就如同一个经常干坏事的好人,虽然它的本意是想做些好事情,但是很多不好事情的事情确实是发生在测试代码中,我补充一些我的观点, 下面这些场景以我为主角,

  1. 片面追求测试覆盖率, 导致写了很多类似于 assert 1==1 之类的测试代码;

  2. 大量的使用 mock, stub 之类的东西 (很多时候为了解耦测试代码,又不得不用这些东西),虽然测试通过了,但是上线后,系统被残酷的线上环境虐成渣渣;

  3. 自认为自己写的测试代码很清晰, 可以当作文档,然后就真的不写文档了,但是过一段时间后,发现读测试代码比读源码还难;

  4. 写了些测试代码后,盲目地自信,整天惦记着重构的事,当真的要重构时,却发现项目代码和测试代码要一块重构, 每天改测试代码改的要死;

  5. 写了些测试代码后,盲目地自信,认为打造一个 自动化 (automatic), 反复进行 (regression tests) 的测试系统很简单,并且幻想这个自动化测试系统会很可靠,于是每次通过 CI 成功提交代码后,都会自欺欺人地说: 妥妥的. 然后第二天被老板骂的找不到北。 打造一个可靠的自动化测试系统,需要一个研发团队持续不断的努力,需要大量的工具, 流程和文档,而不只是一些测试代码。

#42 楼 @yue 我说 twitter 哇哈

@hooopo 你们公司是不是来了一个让你头疼的开发,哈哈

没有完全的语言,楼主。 用之所长,必之所短。

#43 楼 @kayakjiang

个人体会:

  • 对测试代码本身的质量不需要要求太高,覆盖到好过没覆盖到
  • 测试作为接口文档的功能很可疑,因为测试代码质量下降的很快
  • 差的测试好过没有测试
  • 测试只是质量保证的一个环节,有比没有好
  • 有 CI 比没 CI 好
  • 没测试真不敢重构
  • 尽量靠接口的解藕,尽量克制使用 mock
  • 测试代码中重复代码好过封装重复,上下文更明晰

滥用 Gem 包 滥用 default scope

好多人过度的追求语法糖,可是连基本的业务逻辑都搞不通,各种埋坑,手越高,坑越深

#47 楼 @fleuria 你说了个人体会,这点我是赞同的,关于测试这块的一些评价我也是出于自己的个人经验有感而发的,我其实有一段非常长的写测试的经历,我对写测试的熟练程度可能比这个坛子里大多数人都要多些,并且我现在仍然会对系统的一些比较重要,比较核心的地方写测试。你说的覆盖到好过没覆盖到, 差的测试好过没有测试 等等这些我认为是写测试的忌讳,写测试是一件很耗费精力的事情,这个精力既包括脑力,也包括体力,程序员的精力既是有限的,也是宝贵的,把程序员的这些宝贵精力消耗在差的测试上,或者是一些并不是很重要的代码的覆盖上岂不是一种严重浪费,当然如果你觉得自己精力好,或者觉得项目的每一个地方都很重要,都不能有任何闪失,你可以继续这样做。以我的个人经验做个小结:如果决定写测试,那务必就写出质量好的,能够用在刀刃上的测试代码, 如果做不到,还不如前期认认真真思考,做好全面的设计,忘记重构,忘记以后再去改善代码质量等等之类将来的事情,并且索性大方点把测试的任务交给 QA 团队去做。

元编程是个好东西,但是要看情况,看场景具体使用,元编程有的方法也会有性能问题,不能盲目使用。

元编程我是在设计 DSL 时采用的,为的是让脚本写起来顺心~

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册