Dynamic Class Methods in Ruby

Rails has a whole slew of class methods that really should be instance methods. The most notable of these methods is ActiveModel::Naming‘s model_name, which is a class method that is expected on every ActiveModel object (for example, it is expected by form_for). This code uses that as an example, but the pattern applies anywhere.

If you want to make an ActiveModel factory, which is a boring and classic design pattern, then you need to deal with the fact that the model_name must be both a class method and different for each instance of your factory. That is:

a = ActiveModelFactory.new("hello")
b = ActiveModelFactory.new("goodbye")
a.class.model_name.should == "hello"
b.class.model_name.should == "goodbye"
a.class.model_name.should == "hello"

Here’s how that looks:

class ActiveModelFactory
  def initialize(model_name)
    @model_name = model_name
  end

  def class_with_model_name
    self.class_without_model_name.tap do |c|
      c.instance_variable_set('@_model_name', @model_name)
      (class << c; self; end).send(:define_method,:model_name) do
        value = self.instance_variable_get('@_model_name')
        model_namer = Struct.new(:name).new(value)
        ActiveModel::Name.new(model_namer)
      end
    end
  end
  alias class_without_model_name class
  alias class class_with_model_name
end

Going through this: the initializer takes the model name and tucks it away in an instance variable, where it belongs. That part is straight-forward.

To make the model_name class method work we redefine the class to be different for each instance. It goes like this:

The class_with_model_name method produces the instance’s class, but first it modifies it. It sets an instance variable on the class itself, @_model_name, which is the same as the instance’s @model_name value. Then it defines a method named model_name on the class. Inside model_name it gets the value of this @_model_name instance variable, which has traveled from the factory instance through the class into an eigenclass and into a define_method block. Once it has this value it builds the struct and produces the ActiveModel::Name object as needed.

To tie this all together we use the alias_method_chain trick, which is used when someone screwed up. Alias class to class_without_model_name, then our class_with_model_name to class.

Elsewhere by me: Unobtrusive Ruby doesn’t make people jump through these hoops. This research was done as part of my liaison gem, which is an ActiveModel factory.

Advertisements
%d bloggers like this: