SULAIR Home

Dynamically defining an object's methods through metaprogramming

Today I began looking into how to dynamically create methods in Ruby for a certain use case I had.

Basically what I wanted to do was to have methods dynamically added to an object based on what was present at the time of instantiation (similar to ActiveRecord).

So:
ab = Obj.new({:a=>"val_a",:b=>"val_b"})
yz = Obj.new({:y=>"val_y",:z=>"val_z"})
Should return objects that can respond to a and b or y and z respectively.
ab.a #=>"val_a"
ab.b #=>"val_b"
  
yz.y #=>"val_y"
yz.z #=>"val_z"

However; this ended up requiring a bit of trickery to accomplish. After doing some research I found the define_method, which ended up being instrumental in my approach. The following ended up greatly improving the cleanliness of my code as well as the ease of using the object.

My object iterates through a parameter hash and matches up existing parameters with defined parameters in a configuration hash. This configuration hash is necessary for the full implementation however it is EXTREMELY important to have for safety purposes. I wanted to turn the resulting keys into methods on the object so we could call Obj.author or Obj.title when that mix of parameters and configuration existed when the object was instantiated.

So I created a method in my object called create_method which is just for convenience.

def create_method(name,&block)
  self.class.send(:define_method,name,&block)
end

Now when I've found matching keys in the parameters and the configuration I can invoke this method.

Hash.each do |key,values|
  self.create_method(field.to_sym){"#{key.to_s.capitalize} = (#{values})"}
end

So this gets us our methods when this is invoked on instantiation. However; we need to easily be able to know which methods will be available in the object without having to iterate over the parameter and configuration hashes again. So I added a keyword_fieds method to the object which holds an array of keys that we called create_method on.

While this provides a way we can get at our methods, it's not easy.

Obj.new({:a=>"val_a",:b=>"val_b"})
Obj.keyword_fields.each do |field|
  "Key=#{field} : Value=#{Obj.send(field)}"
end
# Returns
# Key=a : Value=val_a
# Key=b : Value=val_b

However; this isn't ideal. My specific use case actually has us iterating through this object in a view, and I'm not particularly comfortable with calling send from a view.

This is where implementing a block can really clean up how we interact with this object. I decided to implement an each iterator for our object. Note that I am passing the field back to the block in addition to the field value because in my use case I need that key for a link.

def each(&block)
  self.keyword_fields.each do |field|
    block.call(field,self.send(field))
  end
end

Now, in our UI, we can do the following:

<%- Obj.new({:a=>"val_a",:b=>"val_b"}) -%>
<%- Obj.each do |key,field| -%>
  <span><%= h(field) %></span> <%= link_to("[x]", controller_index_path(params.merge(key=>nil))) %>
<%- end -%>
# Returns
# <span>a</span> <a href="/?b=val_b">[x]</a>
# <span>b</span> <a href="/?a=val_a">[x]</a>

So now we have a UI friendly query object that can be easily iterated through in the UI. Because we are returning the key to the block we can reliably create links that can remove those parameters from the URL when those parameters are in the configuration hash.



« Back