« | Main | Forced to disable Firefox Inspect Element due to muscle memory »

February 28, 2012

Metaprogramming Your ActiveRecord Objects at Runtime

I often store Ruby code in the fields of ActiveRecord object, giving me the ability to morph the system's behavior at runtime. Metaprogramming in the large, if you will. This is one of my favorite techniques in the Ruby on Rails sphere and one of the reasons I love working with dynamic languages. I'm not exactly recommending this technique to anyone, since it's akin to juggling live chainsaws. You especially don't want to architect an entire production system this way, since there are negative implications for peformance and testability. However, the implementations can be simple and a little bit here and there have gone a long way for me over the years.

While there are many ways to leverage this technique, today I'm going to cover an advanced style that involves changing metaclass behavior of a particular object instance using a real example from DueProps.

Screen shot 2012-02-28 at 1.37.51 PM
The central domain object in DueProps is the Prop (aka "Award"). Props are virtual tokens of appreciation that are exchanged amongst users. I'm trying to build a game, not greeting cards, so from the inception of the project I've wanted each Prop to exhibit unique behavior. However, Props are produced by my creative team and uploaded to the app as data: a collection of images with fields for description, points and multiplier values, etc. Programmers don't create individual classes of Props in source code, we have one class called Award with many instances.

Six months into production, we already have over 100 unique Prop instances and plan to have thousands over time. Having a class for each one, or even necessarily for each type of Prop won't scale development-wise. Therefore I devised a configuration system that allows us to program behavior related to Props at runtime, without needing to add new attributes to its database table everytime we come up with a cool new idea. Ruby and Rails already have the building blocks for doing exactly that.

Note that I'm not making much of an attempt to generalize the example code presented here. If you are able to wield this kind of technique then you'll be able to figure out how to apply it to your own problem domain. Here we go...

First I'll add a modifier_expr text field to my Award records.

class AddNominationClassExprToAwards < ActiveRecord::Migration
  def change
    add_column :awards, :modifier_expr, :text
  end
end

Now I'll add an after_initialize callback to my Nomination ActiveRecord class, which is what I want to be modified by Award at runtime.

class Nomination < ActiveRecord::Base

  belongs_to :award

  after_initialize :modify,
    if: lambda { award && award.modifier_expr? }

I only want to run this callback if I have an award instance, and only if that award instance has a non-nil nom_modifier value, hence the if expression.

Now the private modify method, which uses instance_eval to alter this object's metaclass object:

  private

   def modify
    instance_eval award.modifier_expr
  end

end

Altering the object's metaclass means that I can add or override its class methods without affecting any other Award instance with my modifications. For example, under normal circumstances Awards don't allow you to nominate yourself (give yourself a Prop) and that rule is enforced with a validation method:

class Nomination < ActiveRecord::Base

  validate :normal_validations

  def normal_validations
    if users.include?(submitter)
      errors.add(:user_ids, "You can't nominate yourself")
    end
  end

end

Note that I've left out other standard validations for clarity, because the one that matters here is the one that I want to override in the example. I named the method normal_validations to remind myself of its intention.

Let's make it so that Teamwork props can be given to yourself and others, but not only to yourself. We'll add this code snippet as its modifier_expr

def normal_validations
  if users.size == 1 && users.include?(submitter)
    errors.add(:user_ids,
      "You can't give Teamwork Props to only yourself")
  end
end

Now when validations run for a Teamwork nomination, my replacement normal_validations method will run instead of the original. Over time, as I build up a history of interesting validations, I may abstract them out into a module, giving myself an in-app API for the behavior of nominations instead of having to code things out explicitly.

Incidentally, I wrote this code very recently using TDD. Here is a gist of the resulting spec:

TrackBack

TrackBack URL for this entry:
http://www.typepad.com/services/trackback/6a00e54fdca9118833016302297650970d

Listed below are links to weblogs that reference Metaprogramming Your ActiveRecord Objects at Runtime:

Sponsored By

Flattr

or visit my my homepage

My Companies

My Latest Books

My Book Series