Guilhermesilveira's Blog

as random as it gets

Posts Tagged ‘reflection

missing_knowledge: dynamically adding ruby methods

with one comment

While creating restfulie we have come across a few issues related to my complete lack of mastery of the ruby meta programming features.

The client’s code (hypermedia aware resource consumer) is based on ruby’s open classes and objects idea: while parsing a xml (or json) format, one can easily add specific methods to that particular object, and that is no news for anyone using ruby, but the interesting bit is that one can do it in several ways, each one with a complete different result.

statically defining methods

The first thought is to define a method for that just deserialized object:

object = self.from_xml
def object.pay
  // execute payment request here
end

This is the easiest approach and works really well if you know which method you want to define. In our case, the name of that method only become available during runtime and it is contained within an string so I can not execute:

object = self.from_xml
method_name = "pay"
def object.method_name
  // execute payment request here
end

In order to dynamically add methods to objects, the simple “def variable_name.method_name … end” path does not sound as a viable alternative.

missing a method?

The second approach was to use the method missing approach. By iterating over the possible state changes/requests of a resource, one can define whether a new method exists or not:

# keeps track of all possible state
# changes and requests
attr_accessor :state_changes
def method_missing(m, *args, &block)
  if state_changes.keys.include? m
    # execute the related http request
  else
    super(m, args, block)
  end
end

Whenever one redefines method_missing, he has to redefine the respond_to? method:

attr_accessor :state_changes
def method_missing(m, *args, &block)
  if state_changes.keys.include? m
    # execute the related http request
  else
    super(m, args, block)
  end
end

def respond_to?(m)
  state_changes.keys.include?(m) || super.respond_to?(m)
end

This is a great (and possibly only) approach to define new methods unknown to the developer at compile time which name is a composition of important strings as with ActiveRecord finders.

The keyword here is new. That’s the reason why method_missings is not the right approach for apis like restfulie. While retrieving a resource from the web, one of its actions might be named destroy, in which case the original destroy method will be invoked, without ever passing through the method_missing once the method does exist.

Appart from that, it is important to notice that this approach is highly valuable for those frameworks which supply dynamic method names for their users as mentioned earlier as ActiveRecord: in those cases, an infinite number of methods is made available for developers.

dynamically defining a method

Become more and more hardcore in the way to define a method, the following step was to check the define_method method. It seems to fit as it would receive a string as the method name and add that method to the object’s class.

class << self
  define_method("pay") do |payment|
    # execute payment here
  end
end

There is one issue here: define_method receives a block as an argument, and blocks cannot receive blocks as arguments in ruby 1.8. It is fine in 1.9, but not in 1.8, so I can not do:

class << self
  define_method("pay") do |payment, &block|
    # execute payment here
  end
end

Another important characteristic of using blocks with the define method, is that it can not access its local context (outter variables), unless you one uses the send method to invoke define_method:

self.class.send :define_method, pay do |payment|
  # access external variables
  # but no body support
end

module eval

The last approach is based on method aliasing. One can evaluate a block of code in a module using the module_eval method:

self.module_eval do
  def fixed_name_method(*args, &block)
    # now i can access the args
    # and the block
    # but the method name was fixed!
  end
end

So we are left with only one task: rename that method from fixed_name to the content of the variable method_name:

method_name = "pay"
self.module_eval do
  def fixed_name_method(*args, &block)
    # now i can access the args
    # and the block
  end
  alias_method method_name, :fixed_name
  undef :fixed_name
end

Note that we also undefined the fixed_name method after creating our desired alias. This is a known path to dynamically add methods to classes bypassing ruby’s 1.8 limitation with blocks receiving blocks as arguments.

Everything would be wonderful just prior to using the api in a multithread enviroment. In this case all those method definitions were added to all existing objects of that class. In this case, the use of instance_eval is indicated.

current method name

At last we come to the last challenge for the client side: once all methods are defined in the same way (within a loop condition) where the evaluation process does not give us access to outter variables, how come we know which method was invoked?

Googling around, the following method can be created in order to discover which method is being invoked:

def current_method
  caller[0]=~/`(.*?)'/
  $1
end

the next step

This is the path we went while creating restfulie as I had no prior hardcore experience with ruby’s metaprogramming techniques. It is clear that using such api allows one to dynamically extend classes and objects in order to achieve that “one extra dimension” of generalization to our code.

Advertisements

Written by guilhermesilveira

November 9, 2009 at 1:00 pm