2

我正在修补 Array 以添加我自己的方法Array#left_outer_join。因为我是 Ruby 新手,所以我想做一些漂亮的事情,并且有一个方法可以返回一个新数组和一个 bang 方法来替换我当前的数组。然而,我最终编写了两次相同的代码,非爆炸和爆炸方法之间的唯一区别是调用 map 或 map!分别。

我已经阅读了一些关于块和元编程的内容,最后得到了以下内容:

class Array
  def left_outer_join_proc(method, ary, &block)
    self.send(method) do |obj1|
      ary.each do |obj2|
        if yield obj1, obj2
          obj2.keys.each do |key|
            obj1[key] = obj2[key]
          end
          break
        end
      end
      obj1
    end
  end

  def left_outer_join(ary, &block)
    left_outer_join_proc(:map, ary, &block)
  end

  def left_outer_join!(ary, &block)
    left_outer_join_proc(:map!, ary, &block)
  end
end

events.left_outer_join!(users) {|event, user| event['user_id'] == user['user_id'] }

到目前为止,这工作得很好,Object.send是(根据 SO)动态调用方法的最佳用途,我有点喜欢这种方法(尽管我的纯粹主义者讨厌Array用第三种方法污染类)。

现在的问题是:定义非爆炸和爆炸方法并使其保持干燥的最佳实践是什么?

编辑:这个问题不是关于“爆炸方法是否意味着破坏性方法?” 但实际上是关于“如果我要写Array#add_twoand Array#add_two!,如何确保我不必定义一个方法 withmap{|x| x +2 }和另一个 with map!{|x| x + 2 }.

我知道我可以使用

def add_two!(x)
  self = add_two(x)
end 

但我要求的是“最佳性能、最佳可读性”类型的答案(查看源代码Array#map查看Array#map!“细微”的性能差异)。

4

4 回答 4

3

您还可以将单个项目的代码分解出来,然后在数组方法中更详细一点。这里的left_outer_join_element()方法本身就具有实际意义,即使对于非数组对象也是可重用的。

def left_outer_join(ary, &block)
  self.map { |e| left_outer_join_element(e, ary, &block) }
end

def left_outer_join!(ary, &block)
  self.map! { |e| left_outer_join_element(e, ary, &block) }
end

protected

def left_outer_join_element(element, ary, &block)
  ary.each do |obj|
    if yield element, obj
      obj.keys.each do |key|
        element[key] = obj[key]
      end
      break
    end
  end
  element
end
于 2013-10-15T09:52:46.537 回答
1

定义非爆炸和爆炸方法的最佳实践是什么

没有适用于所有情况的“最佳实践”。使用你的常识。

例如,map!of array 将就地改变数组。bang 版本的意思是“危险!破坏性方法!”。可能的实现:

def map
  # do the mapping
end

def map!
  @elements = map
end

save!如果操作不成功,ActiveRecord 将引发错误。Bang 的意思是“会引发错误”。

一般来说,方法名称中的感叹号应该警告程序员,“注意,这里可能/将会发生危险的事情”。可能的实现:

def save
  save!
  true
rescue
  false
end

def save!
  # do the saving. Raise error if something goes wrong
end

同样,一个方法的 bang 版本可能有无数用例,因此不可能有单一的模式/方法。

于 2013-10-15T09:19:14.583 回答
1

某些对象(例如Arrays 或Strings)具有允许您将对象的整个内部状态替换为相同类型的另一个对象的状态的方法。这种方法通常被恰当地命名为replace. 对于具有这种方法的对象,您可以简单地实现破坏性方法replace和非破坏性方法:

def foo
  # return new instance
end

def foo!
  replace(foo)
  nil # or self
end

相反,所有对象都有一个创建具有相同内部状态的副本的方法,称为dup. 您可以根据破坏性版本来实现非dup破坏性版本:

def foo!
  # modify internal state
  nil # or self
end

def foo
  dup.foo!
end

但是,在某些情况下,破坏性和非破坏性版本可以通过不同的实施方式来提高效率。在这种情况下,可能无法避免一些重复。

注意

self = add_two(x)

不起作用。它甚至在语法上都不是有效的。即使它语法上是有效的,它也会简单地分配给一个名为的局部变量self而不改变特殊关键字self

于 2013-10-15T12:06:34.443 回答
1

看看这里的“重复”:

  def left_outer_join(ary, &block)
    left_outer_join_proc(:map, ary, &block)
  end

  def left_outer_join!(ary, &block)
    left_outer_join_proc(:map!, ary, &block)
  end

在这种情况下,我会说你已经做了合理的事情。和方法名称是必需的def,对您拥有大部分共享逻辑的 proc 的调用也是如此。尽管字符串编辑方面的差异很小,但差异的位置至关重要。它也很容易阅读和理解您的代码。

为了更进一步,你可以做类似的事情

 ['', '!'].each do |bang_type|
   define_method( "left_outer_join#{bang_type}" ) do |ary, &block|
     left_outer_join_proc( "map#{bang_type}", ary, &block )
   end
 end

但以上将 DRY 概念推向了极端,结果代码更难阅读和调试。

于 2013-10-15T09:26:12.790 回答