Carsten Zimmermann
Posted on March 23, 2023
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 A::Nested::Class
accesses 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.
private_constant
all the things!
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.
Ruby has 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.
(Singleton) Methods Over Constants
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 CONSTANTS
:
- 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.
Enforce Explicit Constant Visibility
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.
Named Classes Are Constants
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 Nested::Class.new
.
And indeed it does, but that will be the topic of the next article. 😀
Cover image credit: DALL·E 2.
Posted on March 23, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.