Ruby's Class.allocate
Today I’m going to discuss the little-known ruby method, Class.allocate.
What is #allocate?
In these situations, so that I don’t miss any details - I like to turn to Ruby’s source, which as of 1.9.3 is:
VALUE
rb_class_new_instance(int argc, VALUE *argv, VALUE klass)
{
VALUE obj;
obj = rb_obj_alloc(klass);
rb_obj_call_init(obj, argc, argv);
return obj;
}
Cool - that’s easy enough of a translation:
class Class
def new(*args)
obj = allocate
obj.initalize(*args)
obj
end
end
So #new is just an allocation followed by a call to initialize. Ruby exposes the allocate method to us decoupled from #initialize!! If you’re an Objective-C developer, you may be saying “haha, us too!”, and you’d be totally right:
[[Something alloc] init];
is commonly used instead of:
[Something new];
That's great, why do I want it?
One of the reasons that Objective-C programmers prefer [[Something alloc] init] is that it makes calling custom initializers much easier. So instead of always calling init, you may want to initWithConnection. We can use it very similarly in Ruby (and ActiveRecord does just that internally).
Imagine we have a “Data” class that is used for just storing some data:
class Data
def initialize
@data = {}
end
end
And then some day we want to be able to initialize an instance of Data already populated.
We have a few options. The most common is probably to add a parameter to the initializer:
class Data
def initialize(data = {})
@data = data
end
end
This works really well, but we’ve modified the public interface here in order to allow for this edge case. It’s easy to imagine a time 20 options down the line where our initializer gets pretty ugly.
Another option is to allow setting of data by any member:
class Data
attr_writer :data
def initialize
@data = {}
end
end
Again - this works, but what about the fact that we’re (1) exposing data to be changed at any point in a classes’ life, and (2) wasting time initializing data to a value inside of #initialize that we will never use.
The way we’d accomplish this with allocate is more factory-like:
class Data
def initialize
@data = {}
end
def self.initialize_stored(data)
obj = allocate
obj.instance_variable_set :@data, data
obj
end
end
That way, people can continue to use #new how they do, and when they want to initialize one with pre-populated data they can use #initialize_stored with no object overhead.