无法理解Ruby的魔力

时间:2011-02-17 04:20:43

标签: ruby-on-rails ruby railscasts

在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参数的消息。感谢

3 个答案:

答案 0 :(得分:9)

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程序员来说,这也令人困惑。我强烈推荐“Metaprogramming Ruby”这本书可能是我在ruby中读过的最好的书,它会告诉你很多关于这个神奇的东西。

答案 1 :(得分:3)

Michael Papile的回答基本上是正确的。但是,我想详细说明一下,因为您可能希望了解一些技术细微差别。我查看了 railscasts factory_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/544868740c3e26d8a5e8337940f9de4990b1cd0b/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/9fb8a3b40f24f0c8477776133a2f9cd654ca1c8c/lib/factory_girl/syntax/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中的工厂调用实际上是调用一个方便的方法来调用“默认策略”,它位于同一个文件中:

第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 的对象。 查找方法解析为:

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

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

https://ryanb/railscasts/blob/d124319f4ca2a2367c1fa705f5c8229cce70921d/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

:创建是默认策略。我们返回 factory_girl 查找创建的def。

https://thoughtbot/factory_girl/blob/9fb8a3b40f24f0c8477776133a2f9cd654ca1c8c/lib/factory_girl/syntax/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 

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

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 :: Create ,这里定义:

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

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

回到运行方法,我们看到最初传递到 Factory 便捷方法的所有额外args(在这种情况下,:admin => true )现在被标记为覆盖代理对象然后调用 set 方法,将每个属性名称/值对传递为args。

set()方法是构建类的一部分,代理的父类。

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

第12-14行:

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

这里@instance引用代理对象,即用户工厂对象。

然后,:admin => true 设置为railscast规范代码创建的用户工厂的属性。

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

Michael Papile写道:

  

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

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

答案 2 :(得分:-1)

我认为第二个片段不是该函数的定义。函数定义包含defend。我认为第二个片段看起来像是使用:user参数调用的函数或方法,以及带有f参数的块。

当然,对于元编程,你永远无法确定到底是什么