无法理解Ruby的魔力

发布于 2024-10-18 04:45:11 字数 303 浏览 3 评论 0原文

在railscasts项目中你可以看到这样的代码:

before(:each) do
  login_as Factory(:user, :admin => true)
end

该函数的相应定义是:

Factory.define :user do |f|
  f.sequence(:github_username) { |n| "foo#{n}" }
end

我无法理解admin参数是如何传递给函数的,而在函数中没有关于admin参数的字样。谢谢

In railscasts project you can see this code:

before(:each) do
  login_as Factory(:user, :admin => true)
end

The corresponding definition for the function is:

Factory.define :user do |f|
  f.sequence(:github_username) { |n| "foo#{n}" }
end

I can't understand how the admin parameter is passing to function, while in the function there's no word about admin parameter. Thanks

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

无声静候 2024-10-25 04:45:11

Factory.define 不是一个函数定义,它是一个方法,它接受一个符号或字符串(在本例中为用户)和一个定义您正在创建的工厂的块。 Factory(:user, :admin => true) 创建一个具有管理属性的 User 对象。它不是调用第二个片段中的代码,而是调用 Factory() 来初始化工厂,并选择一个工厂(在本例中是第二个片段中定义的工厂)。然后它也将选项以哈希形式传递给 Factory。

Factory 选择非常通用的 :user 工厂。选项 :admin=>true 只是告诉 Factory 将 User 上的 admin 实例变量设置为 true。

This is actually what it is calling in factory.rb in factory girl

def initialize(name, options = {}) #:nodoc:
  assert_valid_options(options)
  @name = factory_name_for(name)
  @options = options
  @attributes = []
end

所以Factory(name,options)在这段代码中相当于Factory.new(name,options)。

http://www.ruby-doc.org/core/classes/Kernel.html 注意数组和字符串等具有类似的结构。我现在正试图弄清楚他们是如何做到的。

即使对于优秀的 Ruby 程序员来说,这也会让人感到困惑。我强烈推荐《Ruby 元编程》这本书,它可能是我读过的关于 Ruby 的最好的书,它告诉你很多关于这个神奇的东西。

Factory.define is not a function definition, it is a method that takes a symbol or string (in this case user) and a block that defines the factory you are making. Factory(:user, :admin => true) makes a User object, with admin attributes. It is not calling the code in your second snippet, it is calling Factory() which initializes a factory, and selects one (in this case the one defined in second snippet). Then it passes options in hash form to Factory as well.

Factory selects the :user factory which is very generic. The option :admin=>true just tells Factory to set the admin instance variable on User to true.

This is actually what it is calling in factory.rb in factory girl

def initialize(name, options = {}) #:nodoc:
  assert_valid_options(options)
  @name = factory_name_for(name)
  @options = options
  @attributes = []
end

So Factory(name,options) is equivalent to Factory.new(name,options) in this code.

http://www.ruby-doc.org/core/classes/Kernel.html Notice Array and String etc have similar constructs. I am trying to figure out how they did that now.

This is all confusing even for decent Ruby programmers. I recommend strongly the book "Metaprogramming Ruby" It is probably the best book I have read in ruby and it tells you a lot about this magic stuff.

断爱 2024-10-25 04:45:11

迈克尔·帕皮尔的回应基本上是正确的。不过,我想详细说明一下,因为您可能希望了解一些技术上的细微差别。我查看了 railscastsfactory_girl 的代码,我相信还有一些额外的部分可以解释 :admin => 是如何实现的。 true arg 最终创建用户工厂的 admin 属性。属性添加实际上并不是通过Factory的initialize()方法发生的,尽管Michael指出该方法确实是在构建新的用户工厂对象的服务中被调用的。

我将在本说明中包含我采取的所有步骤,以防您想了解如何调查您可能遇到的类似问题。

由于您的原始帖子日期为 2 月 17 日,因此我查看了与该日期非常匹配的 railscasts 版本。

我查看了它的 Gemfile:

https://github.com/ryanb/railscasts/blob/ d124319f4ca2a2367c1fa705f5c8229cce70921d/Gemfile

第 18 行:

gem "factory_girl_rails"

然后我检查了与 2 月 17 日日期最匹配的 factory_girl_rails 提交。

https://github.com/thoughtbot/factory_girl_rails/blob/544868740c3e26 d8a5e8337940f9de4990b1cd0b/factory_girl_rails。 gemspec

第16行:

s.add_runtime_dependency('factory_girl', '~> 2.0.0.beta')

factory_girl版本2.0.0.beta其实不太好找。没有具有该名称的 github 标签,因此我只是查看了最接近的提交日期。

https://thoughtbot/factory_girl/blob/9fb8a3b40f24f0c8477776 133a2f9cd654ca1c8c/lib/factory_girl/语法/vintage.rb

第122-128行:

# Shortcut for Factory.default_strategy.
#
# Example:
#   Factory(:user, :name => 'Joe')
def Factory(name, attrs = {})
  Factory.default_strategy(name, attrs)
end

位于同一文件中:

因此railscasts中的Factory调用实际上是调用一个调用“默认策略”的便捷方法,该方法 39-52:

# Executes the default strategy for the given factory. This is usually create,
# but it can be overridden for each factory.
#
# Arguments:
# * name: +Symbol+ or +String+
#   The name of the factory that should be used.
# * overrides: +Hash+
#   Attributes to overwrite for this instance.
#
# Returns: +Object+
# The result of the default strategy.
def self.default_strategy(name, overrides = {})
  self.send(FactoryGirl.find(name).default_strategy, name, overrides)
end

请注意,调用 FactoryGirl.find 来获取要调用 default_strategy 的对象。 find 方法解析为:

https:// Thoughtbot/factory_girl/blob/9fb8a3b40f24f0c8477776133a2f9cd654ca1c8c/lib/factory_girl/registry.rb

第 12-14 行:

def find(name)
  @items[name.to_sym] or raise ArgumentError.new("Not registered: #{name.to_s}")
end

这里的名称是 :user。因此,我们希望在 user 工厂上调用 default_strategy 。正如 Michael Papile 指出的那样,这个用户工厂是由您最初认为是 Factory 的类定义的railscasts 代码定义和注册的。

https://ryanb/railscasts/blob/d124319f4ca2a2367c1fa705f5c8229cce70 921d/spec/factories.rb

第 23-25 行:

Factory.define :user do |f|
  f.sequence(:github_username) { |n| "foo#{n}" }
end

因此,在调查用户工厂的默认策略时,我环顾了 Railscasts 项目,发现了这一点:

https://ryanb/railscasts/blob/d124319f4ca2a2367c1fa705f5c8229cce70921d/spec/factories.rb

第 43-45 行:

def default_strategy #:nodoc:
  @options[:default_strategy] || :create
end

:create 是默认策略。我们回到factory_girl来查找create的定义。

https://thoughtbot/factory_girl/blob/9fb8a3b40f24f0c8477776 133a2f9cd654ca1c8c/lib/factory_girl/语法/methods.rb

第37-55行:

# Generates, saves, and returns an instance from this factory. Attributes can
# be individually overridden by passing in a Hash of attribute => value
# pairs.
#
# Instances are saved using the +save!+ method, so ActiveRecord models will
# raise ActiveRecord::RecordInvalid exceptions for invalid attribute sets.
#
# Arguments:
# * name: +Symbol+ or +String+
#   The name of the factory that should be used.
# * overrides: +Hash+
#   Attributes to overwrite for this instance.
#
# Returns: +Object+
# A saved instance of the class this factory generates, with generated
# attributes assigned.
def create(name, overrides = {})
  FactoryGirl.find(name).run(Proxy::Create, overrides)
end 

创建策略调用此处定义的run方法:

https://thoughtbot/factory_girl/blob/9fb8a3b40f24f0c8477776133a2f9cd654ca1c8c/lib/factory_girl/factory.rb

第 86-97 行:

def run(proxy_class, overrides) #:nodoc:
  proxy = proxy_class.new(build_class)
  overrides = symbolize_keys(overrides)
  overrides.each {|attr, val| proxy.set(attr, val) }
  passed_keys = overrides.keys.collect {|k| FactoryGirl.aliases_for(k) }.flatten
  @attributes.each do |attribute|
    unless passed_keys.include?(attribute.name)
      attribute.add_to(proxy)
    end
  end
  proxy.result(@to_create_block)
end

此代码内容的翻译/总结首先

,通过在 proxy_class 上调用 new 来构建 proxy 对象,在本例中为 Proxy::Create< /strong>,定义如下:

https://thoughtbot/factory_girl /blob/9fb8a3b40f24f0c8477776133a2f9cd654ca1c8c/lib/factory_girl/proxy/create.rb

基本上你需要知道的是 proxy 正在构建一个新的用户工厂对象并在工厂对象之前和之后调用回调被创建。

回到 run 方法,我们看到最初传递到 Factory 便捷方法的所有额外参数(在本例中,:admin => true)现在被标记为覆盖。然后,代理对象调用set方法,将每个属性名称/值对作为参数传递。

set() 方法是 Build 类的一部分,该类是 Proxy 的父类。

https://thoughtbot/factory_girl/blob/9fb8a3b40f24f0c8477776133 a2f9cd654ca1c8c/lib/factory_girl/ proxy/build.rb

第12-14行:

def set(attribute, value)
  @instance.send(:"#{attribute}=", value)
end

这里@instance指的是代理对象,即用户工厂对象。

这就是 :admin => 的方式。 true 设置为 Railscasts 规范代码创建的用户工厂的属性。

如果你愿意,你可以谷歌“编程设计模式”并阅读以下模式:工厂、代理、构建器、策略。

迈克尔·帕皮尔写道:

http://www.ruby-doc.org/core/classes /Kernel.html 注意数组和
字符串等具有类似的结构。我想弄清楚他们是如何
现在就这么做了。

如果您仍然好奇,您在内核文档中看到的数组和字符串实际上只是用于创建这些类型的新对象的工厂方法。这就是为什么不需要new方法调用。它们本身实际上并不是构造函数调用,但它们确实分配和初始化 Array 和 String 对象,因此在幕后它们相当于在这些类型的对象上调用initialize()。 (当然,在 C 语言中,不是 Ruby)

Michael Papile's response is essentially correct. However, I'd like to elaborate upon it a bit as there are some technical nuances that you might wish to be aware of. I looked over the code for railscasts and factory_girl and I believe there are a few extra pieces to the puzzle that explain how the :admin => true arg ends up creating the admin attribute of the user factory. The attribute addition does not actually happen by way of Factory's initialize() method, although as Michael pointed out that method is indeed being called in service of building the new user factory object.

I'm going to include in this explanation all the steps I took in case you'd like to see how to go about investigating similar questions you might have.

Since your original post is dated Feb. 17th I looked at the version of railscasts that closely matches that date.

I looked in its Gemfile:

https://github.com/ryanb/railscasts/blob/d124319f4ca2a2367c1fa705f5c8229cce70921d/Gemfile

Line 18:

gem "factory_girl_rails"

I then checked out the commit of factory_girl_rails that most closely matched the Feb 17th date.

https://github.com/thoughtbot/factory_girl_rails/blob/544868740c3e26d8a5e8337940f9de4990b1cd0b/factory_girl_rails.gemspec

Line 16:

s.add_runtime_dependency('factory_girl', '~> 2.0.0.beta')

factory_girl version 2.0.0.beta was actually not so easy to find. There are no github tags with that name so I just checked out the closest in terms of commit date.

https://thoughtbot/factory_girl/blob/9fb8a3b40f24f0c8477776133a2f9cd654ca1c8c/lib/factory_girl/syntax/vintage.rb

Lines 122-128:

# Shortcut for Factory.default_strategy.
#
# Example:
#   Factory(:user, :name => 'Joe')
def Factory(name, attrs = {})
  Factory.default_strategy(name, attrs)
end

So the Factory invocation in railscasts is actually calling a convenience method which invokes the "default strategy", which is located in the same file:

Lines 39-52:

# Executes the default strategy for the given factory. This is usually create,
# but it can be overridden for each factory.
#
# Arguments:
# * name: +Symbol+ or +String+
#   The name of the factory that should be used.
# * overrides: +Hash+
#   Attributes to overwrite for this instance.
#
# Returns: +Object+
# The result of the default strategy.
def self.default_strategy(name, overrides = {})
  self.send(FactoryGirl.find(name).default_strategy, name, overrides)
end

Note that FactoryGirl.find is invoked to get the object on which to invoke default_strategy. The find method resolves to here:

https://thoughtbot/factory_girl/blob/9fb8a3b40f24f0c8477776133a2f9cd654ca1c8c/lib/factory_girl/registry.rb

Lines 12-14:

def find(name)
  @items[name.to_sym] or raise ArgumentError.new("Not registered: #{name.to_s}")
end

Here the name is :user. Thus we wish to invoke default_strategy on the user factory. As Michael Papile pointed out, this user factory was defined and registered by the railscasts code that you originally thought was the class definition for Factory.

https://ryanb/railscasts/blob/d124319f4ca2a2367c1fa705f5c8229cce70921d/spec/factories.rb

Lines 23-25:

Factory.define :user do |f|
  f.sequence(:github_username) { |n| "foo#{n}" }
end

So in investigating what the default strategy is for the user factory, I looked around in the railscasts project and found this:

https://ryanb/railscasts/blob/d124319f4ca2a2367c1fa705f5c8229cce70921d/spec/factories.rb

Lines 43-45:

def default_strategy #:nodoc:
  @options[:default_strategy] || :create
end

:create is the default strategy. We go back to factory_girl to find the def for create.

https://thoughtbot/factory_girl/blob/9fb8a3b40f24f0c8477776133a2f9cd654ca1c8c/lib/factory_girl/syntax/methods.rb

Lines 37-55:

# Generates, saves, and returns an instance from this factory. Attributes can
# be individually overridden by passing in a Hash of attribute => value
# pairs.
#
# Instances are saved using the +save!+ method, so ActiveRecord models will
# raise ActiveRecord::RecordInvalid exceptions for invalid attribute sets.
#
# Arguments:
# * name: +Symbol+ or +String+
#   The name of the factory that should be used.
# * overrides: +Hash+
#   Attributes to overwrite for this instance.
#
# Returns: +Object+
# A saved instance of the class this factory generates, with generated
# attributes assigned.
def create(name, overrides = {})
  FactoryGirl.find(name).run(Proxy::Create, overrides)
end 

The create strategy calls the run method defined here:

https://thoughtbot/factory_girl/blob/9fb8a3b40f24f0c8477776133a2f9cd654ca1c8c/lib/factory_girl/factory.rb

Lines 86-97:

def run(proxy_class, overrides) #:nodoc:
  proxy = proxy_class.new(build_class)
  overrides = symbolize_keys(overrides)
  overrides.each {|attr, val| proxy.set(attr, val) }
  passed_keys = overrides.keys.collect {|k| FactoryGirl.aliases_for(k) }.flatten
  @attributes.each do |attribute|
    unless passed_keys.include?(attribute.name)
      attribute.add_to(proxy)
    end
  end
  proxy.result(@to_create_block)
end

A translation/summarization of what this code is doing:

First, the proxy object is built by calling new on the proxy_class, which in this case is Proxy::Create, which is defined here:

https://thoughtbot/factory_girl/blob/9fb8a3b40f24f0c8477776133a2f9cd654ca1c8c/lib/factory_girl/proxy/create.rb

Basically all you need to know is that proxy is building a new user factory object and invoking callbacks before and after the factory object is created.

Going back to the run method, we see that all the extra args that were originally passed into the Factory convenience method (in this case, :admin => true) are now being labelled as overrides. The proxy object then invokes a set method, passing in each attribute-name/value pair as args.

The set() method is part of the Build class, the parent class of Proxy.

https://thoughtbot/factory_girl/blob/9fb8a3b40f24f0c8477776133a2f9cd654ca1c8c/lib/factory_girl/proxy/build.rb

Lines 12-14:

def set(attribute, value)
  @instance.send(:"#{attribute}=", value)
end

Here @instance refers to the proxied object, the user factory object.

This then, is how :admin => true is set as an attribute on the user factory that the railscasts spec code creates.

If you want, you can google "programming design patterns" and read about the following patterns: Factory, Proxy, Builder, Strategy.

Michael Papile wrote:

http://www.ruby-doc.org/core/classes/Kernel.html Notice Array and
String etc have similar constructs. I am trying to figure out how they
did that now.

If you are still curious, the Array and String you see in the Kernel doc are actually just factory methods used to create new objects of those types. That's why no new method invocation is needed. They are not actually constructor calls per se, but they do allocate and initialize Array and String objects, and hence under the hood they are doing the equivalent of calling initialize() on objects of those types. (In C though, of course, not Ruby)

差↓一点笑了 2024-10-25 04:45:11

我不认为第二个片段是函数的定义。函数定义有defend。我认为第二个片段看起来像是使用 :user 参数和采用 f 参数的块调用的函数或方法。

当然,对于元编程,你永远无法真正确定到底发生了什么。

I don't think that second snippet is the definition for the function. Function definitions have def and end. I think that second snippet looks like a function or method being called with an argument of :user and a block that takes an f parameter.

Of course with metaprogramming you can never really be sure what the hell is going on.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文