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

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

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

方案 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'])

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

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