新手问题 如何权衡 DRY 与可读性

cn_boris · 2014年12月24日 · 最后由 fsword 回复于 2014年12月26日 · 2004 次阅读

*** UPDATE ***

抱歉之前精简了部分无关的业务代码,可能导致了一定的误导性。现在补上完整的应用场景


比如同一功能的下述两种实现方式,大家觉得使用那种会更恰当?

# 方案1

module ApiHelper
  %w(get post put patch delete).each do |method|
    module_eval(<<-EOF, __FILE__, __LINE__)
      def api_#{method}(params={},headers={}}
        headers.merge!('X-Token' => CFG['api_token']['apollo'])
        #{method}(path,params,headers)
      end
    EOF
  end
end
# 方案 2

module ApiHelper
  def api_get(params={},headers={})
  request_send :get,params,headers
  end

  def api_post(params={},headers={})
  request_send :post,params,headers
  end

  def api_put(params={},headers={})
  request_send :put,params,headers
  end

  def api_patch(params={},headers={})
  request_send :patch,params,headers
  end

  def api_delete(params={},headers={})
  request_send :delete,params,headers
  end

  private

  def request_send(method,params={},headers={})
    headers.merge!('X-Token' => CFG['api_token']['apollo'])
    send method,params,headers
  end
end
共收到 31 条回复

方案1啊

果断方案1, 一目了然

方案2 如果方案一某个代码挂了 你都找不到问题在哪

这个必然方案1

方案一吧,pattern太明显了。

可以结合一下

module ApiHelper
  %w(get post put patch delete).each do |method|
    define_method "api_#{method}" do
      request_send method
    end
  end

  def request_send(method)
    # dosometing
  end
end

方案一

。。。。。。

9楼 已删除

<<-EOF 这个参数是干嘛用的

2 如果,DRY、可读性的目的是为了可维护。第二种更直接,应该更好维护。而且,方法都是固定的,没动态的东西,没觉得没有用元编程的必要。

而且,第二种,方法和第一种方法没什么区别啊。。。只是换一种方法实现罢了。。。

方案一,在你确定你的方法正确的前提下,短小的函数是可以证明不会出错的。

#10楼 @luffycn here doc

@nowherekai 为何在这里需要传 <<-EOF,主要是为了解决什么问题

#6楼 @swordray define_method 由于 context binding 的原因,会比 string eval 要慢

@yfractal

  • 少打字
  • 确保这些方法的定义,不会以后出现某一个方法里面多出来其他方法没有的逻辑

#13楼 @luffycn 意为把EOF作为字符串的结束符,也就是说EOF前的所以内容都将被作为字符串看待。

@cn_boris

def api_#{method}
    #do someting
end

这段代码是写在 EOF 里面,会被当做字符串?

@cn_boris 这就是我最疑惑的地方了,这段代码明明就是在实现一些methods ,但却被当做 string,这些 methods 岂不是没办法被其它的对象调用了

#20楼 @luffycn 元编程 eval 可以把字符串作为代码执行

两种方案我都尝试过!

方案1

只有一个优点,就是是很装逼! 缺点是,维护成本高和可读性低, 尤其是在后期调试的时候(打log 和设置断点,都是问题)。

用软件工程的语言来说, 该方案增加了接口的耦合性, 失去了独立性, 不利于扩展。

方案2

这是软件开发这么多年来的常规做法。 历史经验告诉我们,这么做除了多些几行代码,没有其他坏处! 接口独立,该复用的复用,不该复用的木有复用, 还可以轻松的实现接口内部的差异化! 即使某个程序员只学10分种的ruby, 他也能读懂和维护这段代码! 估计奥巴马都能看懂,这不就是“可读性”三个字的最高追求吗!

很多年前的我,会选择方案1 但此时此刻的我,会选择方案2 也许n年以后,我会再选择方案1 也许随着时间的增长,我的选择也许会不断的改变。 折腾永无止境!

该方案的优劣,因人而异! 如果团队,里面只有你一个人,哪随你折腾! 如果团队,里面有多个人,最好少数服从多数! (如果是偶数,就“拿”石头/剪刀/布 干掉一个先) 因为代码是属于团队,也是写给团队看的,所以应该尽量让多数人接受! 不要搞独裁!

#22楼 @ery 其实两种方式都能用,甚至用在同一个项目中。不过首先要搞清楚主次逻辑。而楼主的代码很明显有一定的误导性,因为 request_send 这个实现逻辑跟 api 放到一起了...

方案一对于api和逻辑放到一起的类是有助于理清代码条理的,因为这些api方法是无关紧要的。断点也不会打到api上,因为这类代码几乎不可能在逻辑上有错。

而方案二则对 api 和实现逻辑分开的情况更有利,比如生成 rdoc。典型案例就是 rest-client,请求逻辑全部是 Request 的类封装了,而 api 则全部放到 RestClient 模块中,这样查阅代码非常方便,也不会弄混淆。

两个方案的区别不重要,问题是这里为什么会有一个公共的方法体? 如果几个method都用一个方法,那这里其实只是一堆方法别名而已。 要真是这样还好,最可怕的是——目前写在一起的代码其实是表面相同,其实这些方法的业务本来是不同的几件事,所以本质上方法体应该是不同的,无论方案1还是方案2,都是在制造耦合......

#22楼 @ery 因为不认识成语读不懂中文,和因为句子不通、逻辑混乱读不懂,是两种情况。一段程序好不好不应该以奥巴马能否读懂为基准,奥巴马读懂了是巧合,我们谈论可读性不应该在这个层面上。

给自己写,我会选方案一,看着爽,给公司写,我会选方案二,代码统计多好多行,也不用担心别人老来问你

@azhao "代码统计多好多行" => 这个理由太强了 :plus1:

同意 #22楼 @ery ,不明白楼上为什么都选1,这里的GET、PUT、DELETE都是HTTP的方法,在业务扩充中几乎是不会增加的,写到一起除了装逼没有什么好处,反而还增加调试难度。

#25楼 @fsword 抱歉,之前的代码精简了部分业务逻辑所以可能会有歧义。我稍后补上整个场景

方案一, 看不懂来问就对了, 对团队成员有好处

方案二增加了一层嵌套调用, 还悄悄污染了方法命名空间, 万一 include module 的 class 或者别的 module 也定义了 request_send 你就 debug 去吧

#30楼 @cn_boris 你现在的例子是添加了这句话吧——

headers.merge!('X-Token' => CFG['api_token']['apollo'])

这里明显是一个前置过滤器,所以还是不能说明这么写的必要性。 我现在有个猜测,也许这个场景本来就不是正常现象,认真分析后,找到的合理做法就不会是这样了......

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