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.