Class Comparison in Ruby
I recently encountered an interesting issue. It boils down to this: How do you know if an object is an instance of a class?
I had to ask this question because I help maintain ActiveInteraction, a
command pattern library. It provides a way to ensure that an object is a
certain class. For instance, if an interaction needs a
User, it could say:
During execution, it’s guaranteed that
someone is in fact a
the scenes, ActiveInteraction validates that using a
case statement. In this
instance, it would look like this:
Turns out that’s not enough for determining if an object is an instance of a
class. In particular, test mocks pretend to be something they’re not by
#is_a? method. Desugaring the
case statement reveals why it
.=== method asks the class if an object is the right class.
the same thing in the other direction by asking the object if it’s the right
class. Since the test mock doesn’t monkey patch the class it’s mocking, the
only way around this is to ask both questions.
While developing a fix for ActiveInteraction, I wondered if there were other ways to do this. I did some research and discovered that there are at least 18 different ways to make this comparison.
It would be unreasonable to make all those checks. In fact, if you’re using
anything other than
#is_a?, you’re doing it wrong. However, I was
interested in creating a class that is indistinguishable from another class. In
other words, the perfect mock.
Creating the Perfect Mock
Before we create the mock, we need to create the class we’ll be mocking.
Next up let’s create the mock. It shouldn’t have anything in common with the class it’s mocking.
With those defined, we can move on to faking the comparisons.
We hit a problem right out of the gate:
This is an issue because it means instances of
FakeCheese won’t be able to
case statements. Unfortunately there’s nothing we can do
about it without monkey patching
Cheese. Let’s stay focused on the
We can do something about this one. Let’s make
FakeCheese behave like
Cheese by delegating to it.
After making that change, we can see that the conditional returns
Note that we broke the default behavior:
FakeCheese aren’t able to pass as
statements anymore. We could fix that by throwing a call to
.===, but remember that we’re trying to build the perfect mock. If
Cheese === american is
FakeCheese === american should be too.
(We’ll see later that falling back to
super doesn’t always make sense.)
Since we can’t make
case statements work without monkey patching, let’s move
on to something we can fix.
We want to delegate to
Cheese again, but this is an instance method. We don’t
have an instance of
FakeCheese. We could make one and delegate to
it, but initializing
Cheese could be complicated or expensive. Let’s turn to
#is_a?’s documentation for inspiration.
trueif class is the class of obj, or if class is one of the superclasses of obj or modules included in obj.
We need a class method that does the same thing. Looking at the documentation
.>=, it seems to fit the bill.
Returns true if mod is an ancestor of other, or the two modules are the same.
.>= we can essentially delegate
Cheese without having an
Let’s reevaluate our conditional to make sure it worked.
Great! That was a little tricky, but ultimately not too bad.
#is_a? do the same thing, they aren’t aliases.
#instance_of? checks for an exact match.
Instead of using a predicate method, we can look directly at the object’s class.
This is the first method where falling back to
super doesn’t make sense.
We can check the classes themselves for equality.
Or slightly stricter equality.
Or the strictest equality.
We can also use the object IDs to compare object equality by hand.
Ruby provides another way to get at the object IDs.
Now that we’ve faked all of the ways to check equality, let’s move on to inequality. The obvious place to start is with the spaceship operator.
.<=>, it doesn’t include
Comparable. So we
have to manually override all of the associated methods.
Another way to see if two classes are the same is to see if they have the same
ancestors. Let’s make
FakeCheese pretend like it has the same family tree as
Having exhausted all of the somewhat reasonable ways to compare classes, let’s move on to comparing their string representations.
.inspect do the same thing, but they aren’t aliased.
.name is just like
.inspect. It’s not aliased either.
Instances of classes in Ruby don’t use their class’s string representation in their own string representation.
Even though we overrode
.name, the instance somehow
uses the class’s real name. So we have to provide a custom
implementation that mimics the default behavior. Other than shifting the
object ID, this is pretty easy.
Unsurprisingly, this is not an alias.
Shorter & More Generic
We created the perfect mock, but it took a lot of code and we repeated ourselves quite a bit. We can make it a lot simpler. Let’s write a function that takes a class and returns a perfect mock of that class.
We can replace all our work above with just one function call.
And it passes all the checks!
[Originally posted to my blog.]
Taylor Fausak was born in California, but he got to Texas as soon as he could. He studied Computer Science at the University of Texas at Austin before entering the wild world of software development. After a brief stint at Cisco, he started his career at Famigo working on all aspects of web development. Then he swapped his Django experience for a chunky slice of Rails bacon and joined OrgSync in the fall of 2012. When he's not slinging code around, he likes riding bikes, playing Magic, and throwing frisbees.
comments powered by Disqus