If you're not familiar with the Null Object pattern, this will be an eye opener. Before I jump into Mimic, let's just get on the same page...
I've written about this idea in: Avoiding errors when forwarding to missing objects
BUT you don't need to jump off to read that. Here's a super quick explanation of a problem that could be solved with a Null Object.
You've used a method that returns an array of objects. Let's call ours gimme
.
You run gimme
and it shoots off to the internet or your database or somewhere and returns a collection of 3 people. Or so you think.
You want to iterate over those people and print out their names so you write some code to do it:
gimme.each { |person|
print person.name
}
Unfortunately, when you run your code it blows up. One of those person objects doesn't respond to name
and you get an error:
NoMethodError (undefined method `name' for nil:NilClass)
Ugh. One of those objects was nil
and doesn't have a name
.
Who knows where the problem is (it is made up after all) but something like a Null Object might make things work better for you.
A Null Object could stand in place of that nil
object in your collection. Your solution would be that the Null Object instance would respond to name
and return something useful. Perhaps it says "oopsie! this one is nil" or maybe it returns an empty string, or whatever makes sense for your application.
The solution could come in different ways but one might be to change the code to do print PossiblyAPerson(person).name
... or something like that. Wrap the object to make sure that print
won't blow up when it tries to output person.name
.
Enter Mimic
Here's the description from the source code:
Copy a class's instance interface to an anonymous, new object that acts as a substitutable mimic for the class
This is telling us that Mimic will create a new object that acts just like another one; exactly what we want from a Null Object.
I wanted to see how it works. So I dived in to the source.
I started reading Mimic source code as of commit f0ef642d351064f047fd07f1ef97dbb94bff15e6
.
I first looked at mimic.rb, which was kind of boring.
require 'securerandom'
require 'mimic/preserved_methods'
require 'mimic/subject_methods'
require 'mimic/class'
require 'mimic/build'
require 'mimic/void'
require 'mimic/remove_methods'
require 'mimic/void_methods'
require 'mimic/mimic'
It merely requires other files. This means I'll need to go hunting around to find the interesting stuff.
What I did find interesting about this file is that the project's namesake is required last: 'mimic/mimic'
. This means that it's likely that the Mimic
module will be able to safely depend on things required above it.
Regardless, I started reading mimic/preserved_methods.rb
and mimic/subject_methods.rb
first.
They were not exciting. For example, I don't yet know why this code is relevant or how it's used:
module Mimic
def self.preserved_methods
@preserved ||= (Object.instance_methods << :method_missing).sort
end
end
Then I saw this in mimic/subject_methods.rb
and at least something looked familiar from the first file I read:
module Mimic
def self.subject_methods(cls)
instance_methods = cls.instance_methods.sort
instance_methods - Mimic.preserved_methods
end
end
This one uses Mimic.preserved_methods
which was the first one I read. So at least we're beginning to make a connection between the files.
Then I hit some interesting parts in mimic/class.rb
.
Feel free to tag along and look at the source yourself but I'm going to break it into parts to figure out what's interesting and what we can learn.
First things first, the top of the file:
module Mimic
module Class
def self.build(subject_class, &blk)
define_class(subject_class, &blk)
end
This is a module definition. So it could mean we could include
or extend
using Mimic::Class
elsewhere in this source code or perhaps the project that pulls this library in might do that.
But the first method we see uses def self.build
which means we'll really only be using it with Mimic::Class.build
It appears to be just a convenient name, however. The build
method takes a class and a block and just passes it on to another method.
If you're familiar with the Forwardable library from Ruby's standard library (which I wrote about here in Ruby Forwardable deep dive) you'd know that you could also write this same method like this:
require 'forwardable'
module Mimic
module Class
extend SingleForwardable
single_delegate [:build] => self
The array of forwarded methods could grow easily without needing to write each method out yourself. That's weird code, though. Forwarding a message to yourself might make you wonder why all that code is there in the first place, and it would just complicate this code.
Conveniently located below build
is define_class
:
def self.define_class(subject_class, &blk)
mimic_class = ::Class.new(subject_class, &blk)
set_constant(mimic_class, subject_class)
mimic_class
end
And here's where we get into "metaprogramming"...
That Class.new
means we're dealing with metaprogramming. It creates an anonymous class, or in other words a class constant with no name. "Metaprogramming" typically means that the program changes itself when it runs. Sometimes people shorten that and say "code that writes code."
There's not a whole lot to read in this define_class
method beyond creating that anonymous class. But it does point us to a set_constant
method, so let's look at that and see what we learn.
def self.set_constant(mimic_class, subject_class)
class_id = mimic_class.object_id
class_name = class_name(subject_class, class_id)
unless self.const_defined?(class_name, false)
self.const_set(class_name, mimic_class)
end
class_name
end
The interesting things that stand out to me are a method called class_name
and then the code checks if a constant is already defined. If it isn't defined the code will set one.
But because there are several references to the value for class_name
, we should probably take a look at that:
def self.class_name(cls, class_id)
if cls.name.nil?
return "C#{class_id}"
else
return "#{cls.name.gsub('::', '_')}_#{class_id}"
end
end
end
end
This method checks if the received cls
has a name
that is nil, and returns a value or if it is not nil, substitutes some parts of the string and adds the provided class_id
.
When I read code, I often mentally pronounce it. So cls
is a bit of a stumbling block for me. How do I pronounce that? That short name could also lack clarity if this were a longer method. If I were to write something similar to this I would likely use klass
. We can't use class
because that's a keyword in Ruby that would cause the interpreter to expect that it was defining a class. But we're just referencing a class that was passed into this method, so we've got to pick something that's clear and doesn't conflict with a keyword. And at least it doesn't say clazz
. Blech.
So why is it checking if a class name is nil? When would that ever happen?
Anonymous classes have no name. Try running this code:
Class.new.name
The result of that will be nil
.
If we jump back up through the methods we'll see that set_constant
receives subject_class
from where it is called in define_class
. Mimic::Class
doesn't have control over what objects will be sent to the build
or define_class
methods so it needs to protect itself against working with nil
like the name of an anonymous class.
So the class_name
method will return the combination of "C"
and the provided class_id
argument with something like "C1234"
.
If the provided cls
object does have a name, this method will return the name where ::
is replaced with _
and the class_id
appended to the end; from My::Awesome::Stuff
to My_Awesome_Stuff_1234
.
Let's jump back to set_constant
again.
def self.set_constant(mimic_class, subject_class)
class_id = mimic_class.object_id
class_name = class_name(subject_class, class_id)
unless self.const_defined?(class_name, false)
self.const_set(class_name, mimic_class)
end
class_name
end
Everything in Ruby has an object_id
. It's a useful way to get a unique identifier for something.
The class passed in as the mimic_class
argument is the anonymous class from ::Class.new
in define_class
.
So the class_id
will always refer to a new object. There's no worry that we'll generate a class_name
that conflicts with an already existing one because each new object will have it's own object_id
.
Next, that const_defined?
check looks only in the current namespace. By default const_defined?
will look for constants with the given name in the current namespace or any ancestor namespace. When you pass a second argument of false
to const_defined?
it will not search any ancestor namespaces.
Then self.const_set
will set the constant for that anonymous mimic_class
to the name from class_name
in the current Mimic::Class
namespace. This means that a generated name like "C1234"
will live as Mimic::Class::C1234
.
There's one last interesting thing to note about this set_constant
method. It returns a string.
If you compare set_constant
to const_set
, the first returns a string but the latter returns the constant that was set. This could create a point of confusion. I have an early expectation that both of these methods will set constants but what they return may be critical to how I use them in my code. Without knowing the rest of the Mimic code well, it's difficult to say if this is a problem. But it is something to consider when building your own tools like this.
Now that we know how set_constant
and class_name
work, let's go back again, finally, to define_class
.
def self.define_class(subject_class, &blk)
mimic_class = ::Class.new(subject_class, &blk)
set_constant(mimic_class, subject_class)
mimic_class
end
We've seen that the mimic_class
is created with ::Class.new
but that ::
is important to note.
Without that ::
at the beginning of Class
, Ruby would look for the first constant it finds named Class
and use that. Unfortunately that would mean Mimic::Class
because it would look in the current scope first. And in this case Mimic::Class
is a module and not a class so not only would it be the wrong Class
constant, but it also wouldn't respond to new
.
One way to get around needing that would be to name this current module something else. Perhaps Mimic::ClassBuilder
could work since that's what's happening here. Or maybe Mimic::Classes
since this is the namespace used for the generated classes. Or maybe one ::
is just fine to get around figuring out a different name.
These decisions matter and weighing the pros and cons of each decision is an important part of building a useful library that communicates intent and purpose well.
The last bit to note here is that the arguments to Class.new
provide a parent class and a place to define methods for the new class.
If Mimic could know the name of the class it needed to build AND the methods that needed to be defined in that block, the code could look like this:
def self.define_class(subject_class, &blk)
class TheClassName < subject_class
def some_method_definition_from_the_block
# ¯\_(ツ)_/¯
end
end
end
Since Mimic has no way of knowing how you'll use it and what constant names it needs to create... metaprogramming to the rescue! Using Class.new
with a parent class and a block of code is a fantastic way to build up the code that you need to exist when your program runs.
This is one of the things that makes Ruby so flexible and fun.
But there is a danger. The code we've seen in Mimic does a great job of making sure that constants are created in a way that is easy for you to debug later. Each generated class is given a name that makes sense and can be found in a particular namespace. It's important to think through the impact of the metaprogramming techniques that you use to create a program and what you leave behind to debug.
There's more to Mimic but we've gotten though some important aspects of building a Null Object library.
The only part left, after creating the class that you need, is to define what methods should do when called on a Null Object.
Should they return nil
? Should they return an empty string? Should they return a placeholder value or a warning?
When we discussed this problem above we saw PossiblyAPerson(person).name
which we could make return "oopsie! this one is nil"
.
Here's a NotAPerson
class and PossiblyAPerson
method that can support this:
class NotAPerson
def method_missing(*args)
return "oopsie! this one is nil"
end
end
def PossiblyAPerson(object)
if object.is_a?(Person)
object
else
NotAPerson.new
end
end
The problem with that is the NotAPerson
object will respond to any method with "oopsie! this one is nil". Even the methods that Person does not implement will have that response.
When we use a Null Object to stand in for another object, we want it to behave as closely as possible to the real thing without breaking our code. But this code has a method interface that allows literally any method to be called on it. And that could be confusing if we need to take a look at it later and debug some problem.
Let's make our own library like Mimic and call it Imitate. We want it to create Null Object classes that will act like the class we give to it.
module Imitate
def self.new(parent_class, &block)
klass = Class.new(parent_class, &block)
parent_class.instance_methods(false).each do |name|
klass.define_method name do
"oopsie! this one is nil"
end
end
klass
end
end
This code creates an anonymous class and defines every method from the provided parent_class
to return the string we want. And here's how we'd use it:
NotAPerson = Imitate.new(Person)
def PossiblyAPerson(object)
if object.is_a?(Person)
object
else
NotAPerson.new
end
end
Now we'll have objects that act like a person, but provide our warning message for every method.
Mimic does a lot more than Imitate. And now that we've walked through the structure and simple implementation we can start learning more about how Mimic works.
Look back and compare what Imitate doesn't do that Mimic does. Then dive in deeper to Mimic and learn more.
Understanding metaprogramming and both how and when to use it is critical to being able to build useful tools and solve difficult problems in Ruby. I often encorage everyone to build their own tools or know how to contribute to the ones they already use. That's why I'm building the Ruby Metaprogramming MasterClass to help developers develop knowledge and experience.