Let’s say I have a Ruby class that is extended by many subclasses. I want each of these subclasses to have a constant named
FRIENDLY_NAME. Obviously, I can edit each of these classes and add the constant. But, if it’s not specified in the subclass, is there a way to define the constant in the subclass from the superclass?
For example, maybe I could set each subclass’
FRIENDLY_NAME to be the subclass’ class name demodularized and underscored, when
FRIENDLY_NAME is not already defined.
It turns out that the answer is yes!
To expand on this, I had a superclass I wanted to extend many times:
class BaseClass end
I wanted all extended versions of the class to have a
FRIENDLY_NAME constant. When specified explicitly in the subclass, that would be the value for the constant. EG:
class Subclass1 Subclass1::FRIENDLY_NAME => "Bob seems like a friendly name!"
However, maybe I have another subclass where I don’t define the constant. EG:
class Subclass2 Subclass2::FRIENDLY_NAME NameError: uninitialized constant Subclass2::FRIENDLY_NAME from /bundle/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/active_support.rb:79:in `block in load_missing_constant' Caused by NameError: uninitialized constant Subclass2::FRIENDLY_NAME from /bundle/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/active_support.rb:60:in `block in load_missing_constant'
How to add that constant? You can add
self.inherited to the superclass. This method is called each time the superclass is extended. There’s also a method on
const_set, so I could automatically set the constant on the child like this:
class BaseClass def self.inherited(child) child.const_set(:FRIENDLY_NAME, child.name) end end
This works just fine for
pry(main)> Subclass2::FRIENDLY_NAME => "Subclass2"
However, it produces ugly warnings for
(pry):10: warning: already initialized constant Subclass1::FRIENDLY_NAME (pry):3: warning: previous definition of FRIENDLY_NAME was here
Initially I tried using the
const_defined? method to check to see if the
FRIENDLY_NAME constant was already defined. The problem is, the
self.inherited method is invoked before the constant is defined in the subclass. Basically, as soon as Ruby see
< BaseClass it runs
Eventually I found this SO question about how to execute code after a class has been defined. The answer there was to use
TracePoint is a class that allows us to handle events in our code. These could be when a line of code is executed, when we call a method, when an error is raised, etc. What we care about is
:end event, which happens when a class or module definition is finished.
Knowing that, we can update our
self.inherited method like this:
class BaseClass def self.inherited(child) TracePoint.trace(:end) do |t| if child == t.self unless child.const_defined?(:FRIENDLY_NAME) child.const_set(:FRIENDLY_NAME, child.code) end end end end end
Subclass2 have a
FRIENDLY_NAME constant, but neither produce warnings!
Having written all of the above, I’m probably not going to use it. I initially wrote this writeup to answer my own StackOverflow question, but as a commenter points out, this is basically reinventing inheritance for constants in a way that will be difficult for people who haven’t read this post to understand.
An easier way to do this is simply to create a class method like
friendly_name. Override it in classes where I want to, and don’t in those where I do.