Class_eval在每个块内都不起作用

时间:2012-01-14 06:38:16

标签: ruby-on-rails ruby activerecord metaprogramming

我已经定义了一个扩展ActiveRecord的模块。

在我的情况下,我必须使用作为compound_datetime类方法的参数给出的符号生成实例方法。在class_eval块之外调用each但不在其中时调用它;在后一种情况下,我得到一个未定义的方法错误。

有谁知道我做错了什么?

module DateTimeComposer
  mattr_accessor :attrs
  @@attrs = []

  module ActiveRecordExtensions
    module ClassMethods
      def compound_datetime(*attrs)
        DateTimeComposer::attrs = attrs
        include ActiveRecordExtensions::InstanceMethods
      end
    end

    module InstanceMethods
      def datetime_compounds
        DateTimeComposer::attrs
      end

      def self.define_compounds(attrs)
        attrs.each do |attr|
          class_eval <<-METHODS
            def #{attr.to_s}_to()
              puts 'tes'
            end
          METHODS
        end
      end

      define_compounds(DateTimeComposer::attrs)
    end
  end
end


class Account < ActiveRecord::Base
  compound_datetime :sales_at, :published_at
end

当我尝试访问该方法时:

Account.new.sales_at_to

我得到MethodError: undefined method sales_at_to for #<Account:0x007fd7910235a8>

1 个答案:

答案 0 :(得分:3)

您正在define_compounds(DateTimeComposer::attrs)模块定义的末尾调用InstanceMethods。在代码中的那一点,attrs仍然是一个空数组,而selfInstanceMethods模块。

这意味着不会定义任何方法,即使它们是,它们也会绑定到InstanceMethods的元类,使它们成为该模块的类方法,而不是实例方法你的Account课程。

这是因为InstanceMethods模块定义中的方法调用是在ruby解释器看到的时候被评估的,当你调用include ActiveRecordExtensions::InstanceMethods不是。这意味着it is possible to run arbitrary code in the most unusual of places, such as within a class definition

要解决此问题,您可以使用ruby提供的included callback,只要模块包含在另一个模块中,就会调用它:

module InstanceMethods
  # mod is the Class or Module that included this module.
  def included(mod)
    DateTimeComposer::attrs.each do |attr|
      mod.instance_eval <<-METHODS
        def #{attr.to_s}_to
          puts 'tes'
        end
      METHODS
    end
  end
end

作为一个额外的建议,您应该能够通过在调用compound_datetime时简单地定义方法来实现相同的结果,从而消除对attrs全局类变量的依赖。

但是,如果您必须有权访问声明为复合日期时间的字段,则应使用类实例变量,这些变量对于每个类都是唯一的,而不是在层次结构上共享:

module ClassMethods
  def compound_datetime(*attrs)
    @datetime_compounds = attrs
    attrs.each do |attr|
      instance_eval <<-METHODS
        def #{attr.to_s}_to
          puts 'tes'
        end
      METHODS
    end
  end

  def datetime_compounds; @datetime_compounds; end;
end

class Account < ActiveRecord::Base
  compound_datetime :sales_at, :published_at
end

class AnotherModel < ActiveRecord::Base
  compound_datetime :attr1, :attr2
end

Account.datetime_compounds
 => [ :sales_at, :published_at ]

AnotherModel.datetime_compounds
 => [ :attr1, :attr2 ]