在实例方法中动态定义实例方法

时间:2013-08-28 22:05:18

标签: ruby-on-rails ruby user-defined-functions

我有几个类,每个类都定义了各种统计信息。

class MonthlyStat
  attr_accessor :cost, :size_in_meters
end

class DailyStat
  attr_accessor :cost, :weight
end

我想为这些对象的集合创建一个装饰器/演示者,这样我就可以轻松访问有关每个集合的聚合信息,例如:

class YearDecorator
  attr_accessor :objs
  def self.[]= *objs
    new objs
  end
  def initialize objs
    self.objs = objs
    define_helpers
  end

  def define_helpers
    if o=objs.first # assume all objects are the same
      o.instance_methods.each do |method_name|
        # sums :cost, :size_in_meters, :weight etc
        define_method "yearly_#{method_name}_sum" do
          objs.inject(0){|o,sum| sum += o.send(method_name)}
        end
      end
    end
  end
end

YearDecorator[mstat1, mstat2].yearly_cost_sum

不幸的是,在实例方法中无法使用define方法。

替换为:

class << self
  define_method "yearly_#{method_name}_sum" do
    objs.inject(0){|o,sum| sum += o.send(method_name)}
  end
end

...也失败了,因为实例中定义的变量method_name和objs不再可用。在红宝石中有没有一个人可以做到这一点?

2 个答案:

答案 0 :(得分:1)

(编辑:我得到你现在要做的事。)

好吧,我尝试了你可能采用的相同方法,但最终不得不使用eval

class Foo
  METHOD_NAMES = [:foo]

  def def_foo
    METHOD_NAMES.each { |method_name|
      eval <<-EOF
        def self.#{method_name}
          \"#{method_name}\".capitalize
        end
      EOF
    }
  end
end

foo=Foo.new

foo.def_foo
p foo.foo # => "Foo"

f2 = Foo.new
p f2.foo # => "undefined method 'foo'..."

我自己会承认这不是最优雅的解决方案(甚至可能不是最惯用的解决方案),但我在过去遇到类似的情况,其中最直接的方法是eval

答案 1 :(得分:0)

我很好奇你为o.instance_methods得到了什么。这是一个类级方法,通常不能在对象实例上使用,从我所知道的,这就是你在这里处理的内容。

无论如何,您可能正在寻找method_missing,它会在您第一次调用它时动态定义该方法,并允许您将:define_method发送到对象的类。每次实例化一个新对象时都不需要重新定义相同的实例方法,因此method_missing只有在尚未定义被调用方法时才允许您在运行时更改类。

由于您期望某个模式所包含的其他类中的方法名称(即yearly_base_sum将对应于base方法),我建议您编写一个返回的方法匹配模式,如果找到一个。注意:这不会涉及在另一个类上创建方法列表 - 如果您的某个对象不知道如何响应您发送的消息,则仍应依赖内置NoMethodError。这样可以使您的API更加灵活,并且在您的统计信息类也可能在运行时进行修改时非常有用。

def method_missing(name, *args, &block)
  method_name = matching_method_name(name)
  if method_name
    self.class.send :define_method, name do |*args|
      objs.inject(0) {|obj, sum| sum + obj.send(method_name)}
    end
    send name, *args, &block
  else
    super(name, *args, &block)
  end
end

def matching_method_name(name)
  # ... this part's up to you
end