Try and Try Again
Attending MagicRuby this year provided me with many take-aways that I would like to discuss. One of these is that of the NullObject Pattern (NOP). There were a number of great presentations there, and I know this topic probably old hat for some of the longer-bearded fellows, but I really like the idea.
I saw a brief glimpse into the NullObject pattern during Ben Orenstein's talk about refactoring [1]. Essentially, the Null Object pattern allows you to create a neutral object that takes the place of Nil. In fancy terms, the NOP lets the client to ignore the difference between a null operation and a real operation via an abstraction layer.
I know your first question will invariablly be: Why?
The Why
Why would you use the Null Object pattern? Let's go into a simple example.
class Project
attr_reader :supervisor
# assume supervisor looks like:
# ["Harry", "Henderson", "hhendersonfake@me.com"]
def initialize(supervisor)
@supervisor = supervisor
end
def display_name
if !supervisor
supervisor[0] + ", " + supervisor[1] # yuck
else
"No supervisor assigned."
end
end
end
So this is pretty straightforward. We want to be able to display the assigned supervisors' name, and if the supervisor hasn't been set yet, we'd like to display the message "No supervisor assigned." And this code does that pretty well. However, as we build this class out this gets pretty messy. We'll continually have to check for the existence of supervisor. The NullObject pattern helps us to remove this smell.
The How
Alright, so hopefully you are all on board with at least contemplating using the NullObject pattern, but what's next? The first thing we need to do is refactor our class a bit. Supervisor will likely have other methods defined and therefore merits its own class. So let's do that now:
class Project
attr_reader :supervisor
# assume supervisor looks like:
# ["Harry", "Henderson", "hhendersonfake@me.com"]
def initialize(supervisor)
@supervisor = Supervisor.new(*supervisor)
end
Supervisor = Struct.new(:first_name, :last_name, :email) do
def display_name
if !first_name.nil? || !last_name.nil?
first_name + ", " + last_name
else
"No supervisor assigned."
end
end
end
end
That looks a little better; the supervisor class is now isolated and we have more of a clue as to what the supervisor object is responsible for. However, we still have the same issue. We are asking the object if it's nil in order to display the right #display_name. This is where we'll actually create a NullObject and make it respond to the methods we expect supervisor to know.
NullSupervisor = Struct.new() do
def display_name
"No supervisor assigned."
end
end
So now our class, incorporated with the NullObject, looks like this:
class Project
attr_reader :supervisor
# assume supervisor looks like:
# ["Harry", "Henderson", "hhendersonfake@me.com"]
def initialize(supervisor=nil)
@supervisor = supervisor.nil? ? NullSupervisor.new : Supervisor.new(*supervisor)
end
Supervisor = Struct.new(:first_name, :last_name, :email) do
def display_name
first_name + ", " + last_name
end
end
NullSupervisor = Struct.new() do
def display_name
"No supervisor assigned."
end
end
end
What did we just do?
We placed an abstraction layer between Project and 'Supervisor'. Depending on how you initialize Project supervisor will be a class of either Supervisor or NullSupervisor. This difference is transparent to the Client, in this case class Project, and allows you to be more declarative in your code.
** Side note **
The use of Struct.new to reveal the intent of the supervisor array is taken from both Ben, who mentioned something similar in his talk, and from Sandi Metz' book Practical Object Oriented Design [2]
The Downside
The downside that was immediately brought to bear during the Q&A section was that the creation of the NullObject introduces coupling between NullObject and Object (In this case NullSupervisor & Supervisor).
A quick definition of coupling is "the degree to which each program module relies on each one of the other modules."[3] In this case, any time you add a method to Supervisor you must create a mirror of it on NullSupervisor.
Personally, I think if you want to have custom fuctions for a NullObject represenation of a particular data structure, you should actually put some thought into it, so this coupling doesn't bother me too much. It's pretty easy to understand and allows you to write code with authority. However, sometimes this just isn't the right fit, and if it's not, go with something else.
Conclusion
The NullObject pattern can be used in many more cases than this, and in many different ways. Learning about this pattern has helped me to think through the instances where I would normally have just used a conditional. Thinking about how your code works, and what callers will send to your object, will help you write more understandable code.
Thanks for reading.
edit: Edited the gists to pass supervisor in as an object, rather than an Array. Also, set default in initialize params rather than by ternary. Link to comment which pointed this out. Thanks safiire.
References
1.) Ben Orenstein's talk from Scottish Ruby
2.) Practical Object-Oriented Design in Ruby - Sandi Metz
3.) Wiki Entry on Null Object Pattern
I hadn't read all of the additional references section until after I'd finished work on this article. Some really great stuff here. Take special note of Avdi's article, as it covers this topic in more depth.
Additional References
Null Objects and Falsiness - Avdi Grimm
Old school article with very useful graph describing NOP (Java - yuck)
Design Patterns in the Wild: Null Object - Josh Clayton
