I always had mixed feelings towards Ruby constants. First, they're not that constant to begin with. You can reassign a constant at runtime freely and all you get is a warning:
irb(main):001:0> FOO = :bar irb(main):002:0> FOO = :lol (irb):2: warning: already initialized constant FOO (irb):1: warning: previous definition of FOO was here
Contrary to popular belief, a
#freeze won't help, either. Yes, it makes the object you're assigning immutable, but it doesn't prevent another constant assignment:
irb(main):001:0> FOO = "bar".freeze irb(main):002:0> FOO = "baz".freeze (irb):2: warning: already initialized constant FOO (irb):1: warning: previous definition of FOO was here irb(main):003:0> # 🤷
(It would be nice to be able to change warning into an error, but there's certainly no
RUBYOPT flag that I know of.)
But most importantly: unless they're defined in the top level object space, I consider them implementation details. If an instance of
Some::Other::CONST, it crosses 5-6 boundaries (depending on where you start counting). That's not actually respecting that module's privacy and also a violation of the Law of Demeter.
By default, constants are public. Most software engineers are very eager to work with the
private keyword to limit the public API of their instances, but it's rarer to see that same rigor applied to class or module-level constants.
private_constant since basically forever (MRI v1.9.3 to be precise). It accepts one or more symbols referring to defined constants in its scope:
module MyModule FOO = "bar".freeze LOL = :wat? private_constant :FOO, :LOL def self.foo FOO end end
Any attempt to access
MyModule::FOO from outside
MyModule will raise a
NameError ("private constant MyModule::FOO referenced").
Please note that
MyModule.foo still works (and returns the frozen string
"bar") as it only accesses the private constant from within its defining scope.
I mentioned I consider constants implementation details. A named identifier for a magic value, maybe something configurable and set at load time. And sometimes, you want to expose that to other components in your applications.
As shown in the code snippet above, you can always create a singleton method / module function that wraps around a private constant.
In my opinion, methods are in all cases superior to
- you can defer (lazy load) the assignment
- you can memoize (
@class_var ||= ...) what's being assigned
- you can delegate the method call
- it's easier to stub a method than a constant in your tests
- it pairs well with making class/module-level value configurable on the application level, e.g. using a well-known
MyModule.configure(&block)format or by using dry-config
- it feels much more OOP to send a message to an object than working with its constants (also, I always feel that CONSTANTS YELL AT ME in the source code)
Points 1..5 all give you a great forward compatible way to refactor how your magic value is used, all for the small price of making your constants private and adding a getter singleton method around it if you really need to expose it to the outside world right away.
Unfortunately, there is no way to make all constant private by default, but RuboCop includes a
RuboCop::Cop::Style::ConstantVisibility cop to at least make the constant scope explicit.
I like that it makes you stop and think about what you're doing.
Now, constants aren't only referred to by
UPPERCASE identifiers: all class and module names are in fact constants, so you could argue that my criticism about accessing
Some::CONSTANT directly must also extend to a form like
And indeed it does, but that will be the topic of the next article. 😀
Cover image credit: DALL·E 2.