missing_knowledge: dynamically adding ruby methods
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
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=~/`(.*?)'/ $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.
Subscribe to comments with RSS.