Allyl Isocyanate
Allyl Isocyanate

Reputation: 13626

Dynamically defining instance method within an instance method

I have a several classes, each of which define various statistics.

class MonthlyStat
  attr_accessor :cost, :size_in_meters
end

class DailyStat
  attr_accessor :cost, :weight
end

I want to create a decorator/presenter for a collection of these objects, that lets me easily access aggregate information about each collection, for example:

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

Unfortunately define method isn't available from within an instance method.

Replacing this with:

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

...also fails because the variables method_name and objs which are defined in the instance are no longer available. Is there an idomatic was to accomplish this in ruby?

Upvotes: 1

Views: 1248

Answers (2)

Alistair A. Israel
Alistair A. Israel

Reputation: 6567

(EDITED: I get what you're trying to do now.)

Well, I tried the same approaches that you probably did, but ended up having to use 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'..."

I myself will admit it's not the most elegant solution (may not even be the most idiomatic) but I've run into similar situations in the past where the most blunt approach that worked was eval.

Upvotes: 1

Zach Kemp
Zach Kemp

Reputation: 11904

I'm curious what you're getting for o.instance_methods. This is a class-level method and isn't generally available on instances of objects, which from what I can tell, is what you're dealing with here.

Anyway, you probably are looking for method_missing, which will define the method dynamically the first time you call it, and will let you send :define_method to the object's class. You don't need to redefine the same instance methods every time you instantiate a new object, so method_missing will allow you to alter the class at runtime only if the called method hasn't already been defined.

Since you're expecting the name of a method from your other classes surrounded by some pattern (i.e., yearly_base_sum would correspond to a base method), I'd recommend writing a method that returns a matching pattern if it finds one. Note: this would NOT involve making a list of methods on the other class - you should still rely on the built-in NoMethodError for cases when one of your objects doesn't know how to respond to message you send it. This keeps your API a bit more flexible, and would be useful in cases where your stats classes might also be modified at runtime.

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

Upvotes: 0

Related Questions