Today, Caius made a discovery that shocked me.
He had a class, descending from ActiveRecord::Base, with a custom constructor (initialize method). To debug it, he had the constructor raise an exception. In the console, Thingy.new(params) raised the exception as expected. But wotsit.thingies.find_by_field(value) did not. Even though it was instantiating an instance of Thingy and returning it.
“It must not be calling the constructor” he said.
“Rubbish” said I, “it’s a constructor. Constructors are always called. That’s the point of them”.
But as he dug deeper it certainly looked like the constructor wasn’t being called.
And then he found an article explaining that you should never rely on things being set up on in an Active Record constructor. Mainly because Active Record uses allocate to instantiate associated objects. And what is this mysterious allocate? Why explains it all.
To be honest, I’ve got mixed feelings about this. I can see the use of “allocate” – why’s example of marshalling an object makes sense (I’m slightly less sure about the way that Active Record uses it to load associations). But, to my mind, the definition of a constructor is “the code that is always called when an object is created”. So maybe I should just stop thinking of initialize as a constructor and more as an initialiser.
I was aware of, or at the very least not surprised by this. And for whatever reason, I don’t think I’ve ever thought of Ruby’s initialize as a constructor. If you were to override YourClass.new and not call super you’d see something similar I suppose, although you wouldn’t get a new instance either in that case would you?
It would be interesting to know why ActiveRecord calls allocate directly though, it seems as though you’d only do so if you specifically wanted an uninitialised instance, but maybe that’s the point.
It looks to me like #allocate is for situations where an object’s state is about to be specified by some other means (such as when deserialising from an external source) – so whatever happens in #initialise would be overridden.
Which kind of makes sense, as far as ActiveRecord is concerned, IF you hold that an ActiveRecord model is merely a thin wrapper around the state in the database.
Unfortunately, because we also use ActiveRecord models as a holder for business logic (which may require _temporary_ state) the assumption that all a model’s state is in the database is invalid. Another flaw in the “mixing-business-logic-and-persistence” model that ActiveRecord follows.
[...] default value code into your initialize method, it turns out that, under certain circumstances, it may not be called. Instead, move it to [...]