The Return of the Eigenclass
So many times once I’ve learned how to do something, I’ll find myself doing it but maybe not fully understanding what I’m doing. Ruby class method definitions are one of these things for me; I do it all the time but it wasn’t until recently that a light bulb went of and I reached a deeper understanding of what I was REALLY doing. Here is that journey, which started with looking for a way to stub methods in ruby tests.
Taking a Shortcut
While writing some tests in Minitest, I was looking for a way to stub methods on an instance of an object either because the method has side effects I want to avoid in my test or because I want to control the environment of the object under test. Let’s say we have two classes, Runner
and Race
. We want to test Race
, but don’t want all the runners to have to run the full marathon for this test. The implementation could look like:
class Runner
def initialize(name)
@name = name
end
def run_marathon
puts "#{@name} is running a long way"
end
end
class Race
def initialize(runners)
@runners = Array(runners)
end
def start
@runners.each(&:run_marathon)
end
end
runner_m = Runner.new("Martin")
runner_l = Runner.new("Lila")
Race.new([runner_m, runner_l]).start
Running this file gives you:
$ ruby ./runners.rb
Martin is running a long way
Lila is running a long way
Both runners are running the full race. You can “stub” a method on a ruby object by redefining it on the instance of the object. We’ll use this method to make Martin take a shortcut.
class Runner
# Runner hasn't changed...
end
class Race
# Race hasn't changed...
end
runner_m = Runner.new("Martin")
runner_l = Runner.new("Lila")
# Make Martin take a shortcut
def runner_m.run_marathon
puts "#{@name} is taking a shortcut"
end
Race.new([runner_m, runner_l]).start
Running the file now shows Martin taking a shortcut
$ ruby ./runners.rb
Martin is taking a shortcut
Lila is running a long way
Bringing the Class
This is pretty cool! We are using plain old ruby techniques to stub #run_marathon
for Martin so that he doesn’t have to run the full marathon! This is now my go-to stubbing method when writing tests in Minitest or TestUnit. But I recognized that def object.method
syntax from somewhere, hmm. Oh yeah! Defining a class method like def self.method
. This is something we all do every day! Let’s improve the API of our runner object to add a ::named
class method we can use to initialize a new runner instead of calling ::new
directly.
class Runner
def self.named(name)
new(name)
end
# The rest of Runner...
end
class Race
# Race hasn't changed...
end
runner_m = Runner.named("Martin")
runner_l = Runner.named("Lila")
Race.new([runner_m, runner_l]).start
Running this we get
$ ruby ./runners.rb
Martin is running a long way
Lila is running a long way
Great! Everything in Ruby is an object; here Runner
is an instance of Class
. We can use the same notation that we used to redefine a method on an instance of Runner
to redefine a class method on Runner
because it is itself an instance of the Class
object.
class Runner
# No change...
end
class Race
# No change...
end
def Runner.named(name)
new(name + " (participant)")
end
runner_m = Runner.named("Martin")
runner_l = Runner.named("Lila")
Race.new([runner_m, runner_l]).start
$ ruby ./runners.rb
Martin (participant) is running a long way
Lila (participant) is running a long way
We’ve redefined the class method to have “(participant)” after each of the names. What we did here was open up the “singleton” object Runner
and change the named
method. A singleton is a “pattern that restricts the instantiation of a class to one “single” instance” (wikipedia); so there is and will only ever be one instance of Class
representing Runner
. When we first load our ruby program a single instance of Class
is created for Runner
and is accessed via the Runner
constant. These single instances are also called “metaclass” or “eigenclass” (eigen - “own” or “individual”) which are also pretty cool sounding. I might actually watch something called “The Return of the Eigenclass” (would the return just be self? lol).
Here, saying def Runner.named
looks pretty weird. But when used inside a class it looks very familiar to many Rubyists. The way we initially defined ::named
is doing this very thing using self
(and self
just represents the singleton Runner
)
class Runner
def self.named(name)
new(name)
end
# The rest...
end
It turns out, this is common enough that Ruby provides a notation for this. Instead of using def Runner.named
we can use class << object
. That’s class <<
followed by the singleton object.
class << Runner
def named(name)
new(name + " (participant)")
end
end
This leads us to the final step in the journey; understanding the class << self
syntax. I have often seen and understood it as an alternative to writing def self.method
, but I never really felt comfortable with it.
When we use the class << object
notation inside of the runner class we can use self
and so it becomes
class Runner
class << self
def named(name)
new(name + " (participant)")
end
end
def initialize(name)
@name = name
end
def run_marathon
puts "#{@name} is running a long way"
end
end
So now it’s clear; what we are doing here is just opening up a singleton representing the Runner
object so that we can add methods to it using that special Ruby syntax for doing so and just like we did with our more literal def Runner.named
example. Since we are actually inside the Runner
object, we can use self
to access the singleton. Although the syntax is a little more intimidating at first, it’s not as scary once you understand what it’s representing.
The Journey’s End
This is what clicked for me! Just like redefining a method on an instance of Runner
(like we did to make Martin take a shortcut), we are “opening up” the singleton representing Runner
to define additional methods (class methods; or methods on the Runner
singleton).
After discovering this and feeling more familiar, I’ve actually started to prefer it. For one, you don’t have to type self.
as a prefix for each of the class method names. It also keeps the class methods nicely grouped together. Where it really shines for me is when I want to define private class methods.
class Runner
class << self
def named(name)
new(name + suffix)
end
private
def suffix
" (participant)"
end
end
def initialize(name)
@name = name
end
def run_marathon
puts "#{@name} is running a long way"
end
end
We can use the private keyword just like we would in any other Ruby class! This came in handy when I had some complex Rails “scopes” I was defining (they are defined as class methods) and I wanted to break some of the logic out into their own methods for understandabilty (but keep them private, so that they weren’t exposed to the public API).
We now have a great way to stub methods using plain old Ruby, a better understanding of the class << object
notation used for accessing the singleton class of an object instead of using def self.method
, and another great tool in our Ruby toolbox that we can use whenever we need to redefine a method on any instance of an object.