【问题标题】:Can't understand Ruby's magic无法理解 Ruby 的魔法
【发布时间】:2011-06-28 20:14:06
【问题描述】:

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

【问题讨论】:

    标签: ruby-on-rails ruby railscasts


    【解决方案1】:

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

    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 注意 Array 和 String 等具有相似的结构。我想弄清楚他们现在是怎么做到的。

    即使对于体面的 Ruby 程序员来说,这一切都令人困惑。我强烈推荐《Metaprogramming Ruby》这本书,这可能是我读过的最好的 ruby​​ 书籍,它告诉你很多关于这个神奇的东西。

    【讨论】:

    • 我支持元编程 Ruby 书。我是 Ruby 新手,这本书让我对 Ruby 有了更深入的了解。
    【解决方案2】:

    Michael Papile 的回答基本上是正确的。但是,我想详细说明一下,因为您可能希望了解一些技术上的细微差别。我查看了 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"
    

    然后我检查了 factory_girl_rails 的提交,它与 2 月 17 日的日期最接近。

    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 中的 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/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
    

    :create 是默认策略。我们回到 factory_girl 找到 create 的 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 
    

    create 策略调用此处定义的 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 ,这里定义:

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

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

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

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

    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 如何设置为 railscasts 规范代码创建的用户工厂的属性。

    如果需要,您可以在 Google 上搜索“编程设计模式”并阅读以下模式:工厂、代理、构建器、策略。

    迈克尔·帕皮尔写道:

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

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

    【讨论】:

    • @favo - 感谢您的编辑和启用超链接。一旦我意识到创建第一篇文章给了我足够的分数来重新启用我单击“编辑”的链接,突然弹出一个绿色/红色差异显示,显示我想要进行的编辑,并带有适当的编辑评论,不较少的。作为一个 stackoverflow 初学者,我起初认为这是 stackoverflow.com 自动生成的建议,我完全敬畏! :)
    • 欢迎您 :-) 幸运的是,现在您可以在以后的答案中发布更多链接 :-)
    【解决方案3】:

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-07-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-03-01
      • 2013-01-30
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多