Ruby Metaprogramming: Adding Methods to an Instance

07/03/09

When thinking in the abstract, it is hard to imagine why you would want to add a method just to one object and not to a whole class. In practice, I haven’t found a lot of uses for this that haven’t involved working around someone else’s code. Often that code is in the Rails core. I think the folks who built Rails were very clever, and unfortunately they were sometimes ego-driven to prove their cleverness through obscuring the code. I think one great example of this is the association proxy.

If you ask an association proxy what type of an object it is, you get back the answer Array. This is clearly not true since there are a lot of extra things that we get for our relationship buck. Of course you can go to that relationship definition in the parent model and add methods to the association. But what about query results not related to a relationship, but rather a simple find request in ActiveRecord. Let’s say that you want to roll your own pagination, or add other statistics like the type of object in the result set.

It would be sweet if ActiveRecord had added a layer of abstraction to query results … a class inherited from Array that can we can add to without changing the class definition for Arrays everywhere. What would be super sweet, is if there were a series of classes inheritable from the alluded to RecordSet class that were specific a model. So, the User model would have a RecordSet::User model that could be altered at will. If you wanted to add hand-rolled pagination to every RecordSet you could go straight to that Class, and the inherited RecordSet::User would also be affected.

Alas, this is not the case and so my team had to start adding methods to individual query arrays to get the functionality that we needed. For some folks metaprogramming is much of a mystery. So, I wanted to write something that takes an incorrect implementation, explains what it is actually doing and then rights the wrongs. Here we go!

Suppose as was our case, we are doing some serious sql maddness, happily in the confines of AR. We needed some error reporting on the whole set since users were playing with a series of dials and buttons to arrive at the set. So, abstractly what we are looking at is something like this in code:

  set = CrazyModel.all( ... )

We would like the set to have a method ‘errors’, but alas set is of class Array. We don’t want every Array to suddenly have an instance method called ‘errors’. Here is what one of my developers came up with as a first draft:

  set.class.send( :define_method, :errors ) do
    error_text
  end

This worked for making error accessible in the result set, but it had weird results that first showed up in tests. What started happening is that as test were run, result sets further down the line were initiated with the errors for the last set that was modified this way. So, what was happening?

If we look closer at his code, we can see that he actually added an instance method to all arrays. ‘set.class’ gets the class of set, which in this case is an Array. It sends a message to the class asking it to add a method called errors, which is set to the literal contents of ‘error_text’. So, if the last set had an error ‘At least one attribute must be selected.’ Then all array objects now respond to the method errors with that string. In addition to not being thread safe, since the method contains a string literal, and is not a attribute reader, it pollutes the Array instance space.

With metaprogramming there are a lot of ways to skin a method. Here is the way we chose for adding this method just to the instance of ‘set’:

  set.class_eval("
    def errors
      #{error_text}
    end  
  ")
  
  # Note: class eval can also take a block, 
  # but the variable/method 'error_text' 
  # does not exist in that context, so
  # it won't work.