Rails has a whole slew of class methods that really should be instance methods. The most notable of these methods is
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:
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_without_model_name, then our