I thought I had a pretty good understanding of how constant lookup worked in Ruby, but I encountered a surprising piece of behavior recently and I wanted to share it.
We had a god model at work that contains thousands of lines of code, much of which is in methods that aren’t truly core to the model. In a long-term attempt to clean this up, we started by moving some of the methods in this model into dependency mixins, like so:
class User CONSTANT = 10 include User::LoginDependency #lots of methods end
module User::LoginDependency #login-specific methods go in this file end
My pair and I started moving methods over, and it all started out easy enough.
Then we encountered something surprising: a method in one of the mixins referenced a constant from the god model, and we got the error
uninitialized constant User::LoginDependency::CONSTANT.
This, to me, was surprising to see. Ruby, after all, is a dynamic language with late binding, and I expect things like this to be figured out at runtime. My first hand-wavy assumption was that constant lookup works in a manner similar to sending messages to objects, which made me expect that to work.
Turns out, how you name something makes a difference. If I want to have a module called
User::LoginDependency, there are two ways I can define it:
module User module LoginDependency end end module User::LoginDependency end
I can get to both by typing
User::LoginDependency, but the key difference is that in the first case, Ruby understands it as “a module
LoginDependency nested inside of the module
User,” and in the second case, it’s “the module
For the nested version of
User::LoginDependency, if Ruby doesn’t find the constant inside of
LoginDependency, Ruby will work its way up the namespace chain and look for the constant in
User. but in the latter case, there isn’t a next level up that you can look, because
User::LoginDependency is a top-level defined constant.
For this refactoring, my pair and I made the decision that if the new module’s methods were the only ones to reference a given constant we’d move the constant over too, otherwise we’d leave the constant in the original model and update the new dependency mixin to reference the constant’s fully qualified name; e.g.
User::CONSTANT instead of just
Alternatively, we could have switched these mixins over to being defined with a nested namespace, but in our case
User is a class and we didn’t want to be re-opening that class just to add these modules that would later be included. Besides, our approach seemed more appropriate, because it keeps the mixin-specific constants private to that mixin, and the shared stuff is all together in the model that those mixins are all included in.