红宝石中丝锥法的优点

我正在读一篇博客文章,注意到作者在一个片段中使用了 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"
56641 次浏览

使用水龙头,正如博客所做的,只是一个方便的方法。在您的示例中,这可能有点过分了,但是在您希望与用户一起做很多事情的情况下,轻击可以提供一个看起来更干净的界面。因此,也许在下面的例子中这样做会更好:

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

同样,这是有争议的——但是第二个版本看起来有点混乱,需要更多的人工解析来查看所有的方法都是在同一个对象上调用的。

当读者遇到:

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

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

如果是的话:

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

那就一目了然了。读取器不必读取块中的内容就可以知道创建了实例 user

您是对的: 在您的示例中使用 tap是毫无意义的,而且可能不如您的备选方案干净。

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

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

我偶尔喜欢在方法内部使用 tap来有条件地提前返回,否则返回当前对象。

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

所以不是这样:

def some_method
...
some_object.serialize
some_object
end

我们可以节省额外的线:

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

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

我认为使用 tap没有任何好处。唯一的潜在好处,就像 @ Sawa 指出一样,我引用一下: “读者不必读取块中的内容就可以知道创建了一个实例用户。”然而,在这一点上,可以认为,如果您正在执行非简单的记录创建逻辑,那么通过将该逻辑提取到它自己的方法中,可以更好地传达您的意图。

我坚持认为 tap对代码的可读性是一个不必要的负担,可以不使用,或者用更好的技术代替,比如 提取方法

虽然 tap是一种方便的方法,但它也是个人喜好。试试 tap。然后编写一些代码而不使用轻拍,看看你是否喜欢一种方式超过另一种方式。

您可以使您的代码更模块化使用水龙头,并可以实现更好的管理局部变量。例如,在下面的代码中,您不需要在方法的范围内为新创建的对象分配局部变量。注意,块变量 的作用域在块内。它实际上是红宝石代码的优点之一。

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

在函数中可视化示例

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

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

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

我经常看到这样的事故,特别是函数的返回值通常不使用,除了一个黑暗的错误角落。

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

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

@ sawa 回答的另一种说法是:

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

下面两个函数的长度相同,但是在第一个函数中,你必须通读函数的末尾,才能知道为什么我在开头初始化了一个空的 Hash。

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

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

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

这样做的好处是,即使该块返回其他一些结果,Tap 也总是返回它所调用的对象。因此,您可以在现有方法管道的中间插入一个抽头块,而不会中断流。

由于变量的作用域仅限于真正需要变量的部分,因此它产生的代码不那么混乱。此外,块中的缩进通过将相关代码保持在一起使代码更具可读性。

tap的描述是:

将 self 赋值给块,然后返回 self 这种方法的关键是“挖掘”一个方法链,以便执行 在链条中对中间结果的操作。

如果我们 搜索使用 tap的 Rails 源代码,我们可以找到一些有趣的用法。下面是一些项目(不是详尽的列表) ,它们会给我们一些关于如何使用它们的想法:

  1. 根据某些条件将元素附加到数组中

    %w(
    annotations
    ...
    routes
    tmp
    ).tap { |arr|
    arr << 'statistics' if Rake.application.current_scope.empty?
    }.each do |task|
    ...
    end
    
  2. Initializing an array and returning it

    [].tap do |msg|
    msg << "EXPLAIN for: #{sql}"
    ...
    msg << connection.explain(sql, bind)
    end.join("\n")
    
  3. As syntactic sugar to make code more readable - One can say, in below example, use of variables hash and server makes the intent of code clearer.

    def select(*args, &block)
    dup.tap { |hash| hash.select!(*args, &block) }
    end
    
  4. Initialize/invoke methods on newly created objects.

    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. To act on the result of a yield call without having to use a temporary variable.

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

这对于 调试一系列 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

在 ails 中,我们可以明确地使用 tap来列出参数:

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

可能有许多用途和地方我们可以使用 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)你是否曾经发现自己对某个对象调用了一个方法,而返回值却不是你想要的?也许您希望向散列中存储的一组参数添加任意值。使用 大麻更新它,但是返回的是 酒吧而不是 params 散列,因此必须显式返回它。也就是说

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

为了克服这种情况,tap方法应运而生。只需对对象调用它,然后用要运行的代码传递一个块。对象将被屈服于块,然后返回。也就是说

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

还有很多其他的用例,你可以自己找找看:)

来源:
1) < a href = “ http://apidock.com/ails/Object/tap”rel = “ nofollow noReferrer”> API Dock Object tap
2) < a href = “ https://blog.engineyard.com/2015/five-ruby-method-you-should-be-using”rel = “ nofollow norefrer”> five-ruby-method-you-should-be-using

如果您想在设置用户名之后返回用户,则需要执行以下操作

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

有了 tap,你可以省去那个尴尬的回报

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

我将给出另一个我已经使用过的例子。我有一个方法 user _ params,它返回为用户保存所需的 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

在这里,我们可以使用自来水来删除本地变量,并删除返回:

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

有什么区别吗?

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

代码演示:

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

要点:

  • 注意到 u变量现在是如何用作块参数的了吗?
  • 块完成之后,user变量现在应该指向一个 User (用户名为‘ foobar’,并且也保存了这个 User)。
  • 读起来很舒服,也很容易。

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

有一个称为 鞭打的工具可以测量读取一个方法的难度。分数越高,代码越痛苦

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

而且根据弗洛格的结果,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!

在函数式编程模式正在成为最佳实践(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

除了上面的答案之外,我还在编写 RSpecs 时使用了踢踏和嘲弄。

场景: 当我有一个复杂的查询存根和模拟与多个参数,不应该去错过。这里的替代方法是使用 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 })