红宝石中的水龙头方法的优点

时间:2013-07-05 16:12:34

标签: ruby

我刚刚阅读了一篇博客文章,并注意到作者在一段代码中使用了tap

user = User.new.tap do |u|
  u.username = "foobar"
  u.save!
end

我的问题是使用tap的好处或好处究竟是什么?我不能这样做:

user = User.new
user.username = "foobar"
user.save!

或更好:

user = User.create! username: "foobar"

19 个答案:

答案 0 :(得分:92)

当读者遇到:

user = User.new
user.username = "foobar"
user.save!

他们必须遵循所有三行,然后才能认识到它只是创建一个名为user的实例。

如果是:

user = User.new.tap do |u|
  u.username = "foobar"
  u.save!
end

然后就会立即清楚。读者不必阅读块内的内容就知道创建了一个实例user

答案 1 :(得分:32)

使用tap的另一种情况是在返回之前对对象进行操作。

所以不要这样:

def some_method
  ...
  some_object.serialize
  some_object
end

我们可以节省额外的一行:

def some_method
  ...
  some_object.tap{ |o| o.serialize }
end

在某些情况下,这种技术可以节省多于一行并使代码更紧凑。

答案 2 :(得分:23)

正如博客所做的那样,使用tap只是一种方便的方法。在你的例子中可能有点过分,但是如果你想与用户做一些事情,tap可以说可以提供一个更清晰的界面。所以,在一个例子中可能更好,如下:

user = User.new.tap do |u|
  u.build_profile
  u.process_credit_card
  u.ship_out_item
  u.send_email_confirmation
  u.blahblahyougetmypoint
end

使用上述内容可以很容易地快速查看所有这些方法是否组合在一起,因为它们都引用同一个对象(本例中的用户)。替代方案是:

user = User.new
user.build_profile
user.process_credit_card
user.ship_out_item
user.send_email_confirmation
user.blahblahyougetmypoint

同样,这是值得商榷的 - 但是可以证明第二个版本看起来有点混乱,需要更多人工解析才能看到所有方法都在同一个对象上调用。

答案 3 :(得分:14)

这对于调试一系列ActiveRecord链式范围非常有用。

User
  .active                      .tap { |users| puts "Users so far: #{users.size}" } 
  .non_admin                   .tap { |users| puts "Users so far: #{users.size}" }
  .at_least_years_old(25)      .tap { |users| puts "Users so far: #{users.size}" }
  .residing_in('USA')

这使得在链中的任何位置调试都非常容易,无需在局部变量中存储任何内容,也不需要更改原始代码。

最后,在不中断正常代码执行的情况下,将其用作快速且不显眼的调试方式

def rockwell_retro_encabulate
  provide_inverse_reactive_current
  synchronize_cardinal_graham_meters
  @result.tap(&method(:puts))
  # Will debug `@result` just before returning it.
end

答案 4 :(得分:11)

在函数中可视化您的示例

def make_user(name)
  user = User.new
  user.username = name
  user.save!
end

这种方法存在很大的维护风险,基本上隐含的返回值

在该代码中,您确实依赖于save!返回已保存的用户。但是如果你使用不同的鸭子(或者你当前的鸭子),你可能会得到其他东西,比如完成状态报告。因此,对鸭子的更改可能会破坏代码,如果使用普通user确保返回值或使用tap,则不会发生这种情况。

我经常看到这样的事故,特别是那些通常不会使用返回值的功能,除了一个黑暗的马车角。

隐式返回值往往是新手倾向于破坏在最后一行之后添加新代码但没有注意到效果的事情之一。他们没有看到上述代码的真正含义:

def make_user(name)
  user = User.new
  user.username = name
  return user.save!       # notice something different now?
end

答案 5 :(得分:10)

由于变量的范围仅限于真正需要的部分,因此它会导致代码混乱。此外,块中的缩进通过将相关代码保持在一起使代码更具可读性。

Description of tap says

  

为块产生自我,然后返回自我。主要目的   这种方法的目的是“挖掘”一个方法链,以便执行   对链内中间结果的操作。

如果我们search rails source code for tap usage,我们可以找到一些有趣的用法。以下几个项目(非详尽列表)将为我们提供有关如何使用它们的一些想法:

  1. 根据特定条件将元素附加到数组

    %w(
    annotations
    ...
    routes
    tmp
    ).tap { |arr|
      arr << 'statistics' if Rake.application.current_scope.empty?
    }.each do |task|
      ...
    end
    
  2. 初始化数组并将其返回

    [].tap do |msg|
      msg << "EXPLAIN for: #{sql}"
      ...
      msg << connection.explain(sql, bind)
    end.join("\n")
    
  3. 作为使代码更具可读性的语法糖 - 可以说,在下面的例子中,使用变量hashserver使代码的意图更加清晰。

    def select(*args, &block)
        dup.tap { |hash| hash.select!(*args, &block) }
    end
    
  4. 在新创建的对象上初始化/调用方法。

    Rails::Server.new.tap do |server|
       require APP_PATH
       Dir.chdir(Rails.application.root)
       server.start
    end
    

    以下是测试文件

    的示例
    @pirate = Pirate.new.tap do |pirate|
      pirate.catchphrase = "Don't call me!"
      pirate.birds_attributes = [{:name => 'Bird1'},{:name => 'Bird2'}]
      pirate.save!
    end
    
  5. 在不必使用临时变量的情况下对yield调用的结果采取行动。

    yield.tap do |rendered_partial|
      collection_cache.write(key, rendered_partial, cache_options)
    end
    

答案 6 :(得分:10)

如果您想在设置用户名后返回用户

user = User.new
user.username = 'foobar'
user

使用tap你可以保存那个尴尬的回报

User.new.tap do |user|
  user.username = 'foobar'
end

答案 7 :(得分:8)

@ sawa答案的变体:

如前所述,使用tap有助于确定代码的意图(但不一定要使其更紧凑)。

以下两个函数同样长,但在第一个函数中你必须通读结尾以找出我在开始时初始化空哈希的原因。

def tapping1
  # setting up a hash
  h = {}
  # working on it
  h[:one] = 1
  h[:two] = 2
  # returning the hash
  h
end

另一方面,您从一开始就知道初始化的哈希将是块的输出(在这种情况下,是函数的返回值)。

def tapping2
  # a hash will be returned at the end of this block;
  # all work will occur inside
  Hash.new.tap do |h|
    h[:one] = 1
    h[:two] = 2
  end
end

答案 8 :(得分:8)

这是电话链的帮手。它将其对象传递给给定块,并在块完成后返回对象:

an_object.tap do |o|
  # do stuff with an_object, which is in o #
end  ===> an_object

好处是tap总是返回它被调用的对象,即使该块返回其他一些结果。因此,您可以在现有方法管道的中间插入一个点击块而不会中断流程。

答案 9 :(得分:8)

我想说使用tap没有任何好处。唯一的潜在好处,如@sawa points out,我引用:“读者不必阅读块中的内容就知道创建了实例用户。”但是,在这一点上可以提出这样的论点:如果你正在进行非简单化的记录创建逻辑,那么通过将该逻辑提取到自己的方法中可以更好地传达你的意图。

我认为tap对代码的可读性造成了不必要的负担,并且可以在没有或用更好的技术替代的情况下完成,例如Extract Method

虽然tap是一种便利方法,但它也是个人偏好。试试tap。然后在不使用tap的情况下编写一些代码,看看你是否喜欢另一种方式。

答案 10 :(得分:3)

我们可以使用tap的用途和地点数量。到目前为止,我只发现了tap的两次使用。

1)此方法的主要目的是进入方法链,以便对链中的中间结果执行操作。即

(1..10).tap { |x| puts "original: #{x.inspect}" }.to_a.
    tap    { |x| puts "array: #{x.inspect}" }.
    select { |x| x%2 == 0 }.
    tap    { |x| puts "evens: #{x.inspect}" }.
    map    { |x| x*x }.
    tap    { |x| puts "squares: #{x.inspect}" }

2)你有没有发现自己在某个对象上调用一个方法,并且返回值不是你想要的那个?也许您想为存储在哈希中的一组参数添加任意值。您使用哈希。[] 更新它,但是您返回 bar 而不是params哈希,因此您必须明确地返回它。即

def update_params(params)
  params[:foo] = 'bar'
  params
end

为了克服这种情况,tap方法发挥了作用。只需在对象上调用它,然后通过一个包含您想要运行的代码的块来传递。该对象将被生成块,然后返回。即

def update_params(params)
  params.tap {|p| p[:foo] = 'bar' }
end

还有许多其他用例,请自行尝试查找:)

<强>来源:
1)API Dock Object tap
2)five-ruby-methods-you-should-be-using

答案 11 :(得分:3)

你是对的:在你的例子中使用tap是没有意义的,可能不如你的选择那么干净。

正如Rebitzele所说,tap只是一种方便的方法,通常用于创建对当前对象的较短引用。

tap的一个好用例是用于调试:您可以修改对象,打印当前状态,然后继续修改同一块中的对象。例如:http://moonbase.rydia.net/mental/blog/programming/eavesdropping-on-expressions

我偶尔喜欢在方法中使用tap来有条件地在返回当前对象时提前返回。

答案 12 :(得分:1)

在rails中,我们可以使用tap明确地将参数列入白名单:

def client_params
    params.require(:client).permit(:name).tap do |whitelist|
        whitelist[:name] = params[:client][:name]
    end
end

答案 13 :(得分:1)

我将再举一个我使用过的例子。我有一个方法user_params,它返回为用户保存所需的参数(这是一个Rails项目)

def user_params
  params.require(:user).permit(
    :first_name,
    :last_name,
    :email,
    :address_attributes
  )
end

您可以看到我没有返回任何东西,但ruby返回了最后一行的输出。

然后,一段时间后,我需要有条件地添加一个新属性。因此,我将其更改为以下内容:

def user_params 
  u_params = params.require(:user).permit(
    :first_name, 
    :last_name, 
    :email,
    :address_attributes
  )
  u_params[:time_zone] = address_timezone if u_params[:address_attributes]
  u_params
end

在这里,我们可以使用tap删除局部变量并删除返回值:

def user_params 
  params.require(:user).permit(
    :first_name, 
    :last_name, 
    :email,
    :address_attributes
  ).tap do |u_params|
    u_params[:time_zone] = address_timezone if u_params[:address_attributes]
  end
end

答案 14 :(得分:1)

在功能编程模式已成为最佳实践(https://maryrosecook.com/blog/post/a-practical-introduction-to-functional-programming)的世界中,您可以看到tap作为map的单个值,的确可以修改数据在转型链上。

transformed_array = array.map(&:first_transformation).map(&:second_transformation)

transformed_value = item.tap(&:first_transformation).tap(&:second_transformation)

无需在此处多次声明item

答案 15 :(得分:0)

您可以使用tap使代码更加模块化,并可以更好地管理局部变量。例如,在以下代码中,您不需要在方法范围内将局部变量分配给新创建的对象。请注意,块变量 u 在块中作用域。它实际上是ruby代码的优点之一。

def a_method
  ...
  name = "foobar"
  ...
  return User.new.tap do |u|
    u.username = name
    u.save!
  end
end

答案 16 :(得分:0)

有什么区别?

代码可读性方面的差异纯粹是风格上的。

代码演练:

user = User.new.tap do |u|
  u.username = "foobar"
  u.save!
end

要点:

  • 请注意,u变量现在如何用作块参数?
  • 阻止完成后,user变量现在应指向用户(用户名:“ foobar”,并且还会保存谁)。
  • 它令人愉快并且易于阅读。

API文档

以下是易于阅读的源代码版本:

class Object def tap yield self self end end

有关更多信息,请参见以下链接:

https://apidock.com/ruby/Object/tap

http://ruby-doc.org/core-2.2.3/Object.html#method-i-tap

答案 17 :(得分:0)

有一个名为flog的工具,用于测量读取方法的难度。 “分数越高,代码所承受的痛苦就越大。”

def with_tap
  user = User.new.tap do |u|
    u.username = "foobar"
    u.save!
  end
end

def without_tap
  user = User.new
  user.username = "foobar"
  user.save!
end

def using_create
  user = User.create! username: "foobar"
end

根据flog的结果,使用tap的方法最难读(我同意)

 4.5: main#with_tap                    temp.rb:1-4
 2.4:   assignment
 1.3:   save!
 1.3:   new
 1.1:   branch
 1.1:   tap

 3.1: main#without_tap                 temp.rb:8-11
 2.2:   assignment
 1.1:   new
 1.1:   save!

 1.6: main#using_create                temp.rb:14-16
 1.1:   assignment
 1.1:   create!

答案 18 :(得分:0)

除上述答案外,我在编写RSpec时还使用了tap进行存根和模拟。

场景:当我有一个复杂的查询要进行存根和模拟时,请不要忽略多个参数。这里的替代方法是使用receive_message_chain(但缺少详细信息)。

# Query
Product
  .joins(:bill)
  .where("products.availability = ?", 1)
  .where("bills.status = ?", "paid")
  .select("products.id", "bills.amount")
  .first
# RSpecs

product_double = double('product')

expect(Product).to receive(:joins).with(:bill).and_return(product_double.tap do |product_scope|
  expect(product_scope).to receive(:where).with("products.availability = ?", 1).and_return(product_scope)
  expect(product_scope).to receive(:where).with("bills.status = ?", "paid").and_return(product_scope)
  expect(product_scope).to receive(:select).with("products.id", "bills.amount").and_return(product_scope)
  expect(product_scope).to receive(:first).and_return({ id: 1, amount: 100 })
end)

# Alternative way by using `receive_message_chain`
expect(Product).to receive_message_chain(:joins, :where, :where, :select).and_return({ id: 1, amount: 100 })