You probably don’t write code like this:
if object.kind_of?(User)
do_this
else
object.do_that
end
Why not? Because Ruby encourages duck typing and polymorphism.

A hidden version
Here’s the same principle:
if object.nil?
do_this
else
object.do_that
else
This checks that object is of type NilClass instead of type User.
The pattern
There’s an old pattern called Null Object that addresses this special case of avoiding type-checking in favor of duck typing.
Here’s an example of the “Introduce Null Object” refactoring to fix this problem with a Null Object in a Rails app.
The code to be refactored
Airbrake reports an error from this line in a Haml view:
= l location.orders.ascend_by_created_at.first.created_at.to_date
Demeter is displeased but let’s fix the problem for our users first.
The anti-pattern quick fix
We could do something like this:
- if location.orders.any?
= l location.orders.ascend_by_created_at.first.created_at.to_date
- else
No orders yet
Resist the urge.
A cleaner fix
First, re-create the error in the setup phase of a functional or integration test by creating a location without orders, then making an HTTP GET to the page.
We can change the view:
= l location.first_order_date
Much better. Bonus: we’ve given the line an intention-revealing name.
This new method doesn’t exist yet, though, so let’s test-drive it:
context 'first_order_date' do
setup do
@location = create(:location)
end
context 'when an order exists' do
setup do
@date = Date.today
create :order, location: @location, created_at: @date
end
should 'return first order date' do
assert_equal @date, @location.first_order_date
end
end
context 'when no orders exist' do
should 'respond to strftime' do
assert_respond_to @location.first_order_date, :strftime
end
end
end
How should the duck respond
The interesting bit is the ‘no orders’ case. Instead of returning nil, which
caused the original error, we want to return an object that responds to
strftime.
Why? Let’s look at the view again:
= l location.first_order_date
That l method is an alias for localize, which calls
I18n.translate.
If we look inside that method, we’ll see something like:
def localize(locale, object, format = :default, options = {})
unless object.respond_to?(:strftime)
raise ArgumentError
end
# ...
end
The localize method expects an object that responds to strftime.
So, to get the unit test green, let’s do this in the Location model:
def first_order_date
if first_order
first_order.created_at.to_date
else
NullDate.new 'orders'
end
end
private
def first_order
@first_order ||= orders.ascend_by_created_at.first
end
The Null Object
What’s this hipster NullDate object? We’ll have to write it.
The failing test for NullDate tests its strftime method:
context 'strftime' do
setup do
@null_date = NullDate.new('orders')
end
should 'return user-friendly message' do
assert_match /No orders yet/, @null_date.strftime('anything')
end
end
The passing implementation:
class NullDate
def initialize(nullable)
@nullable = nullable
end
def strftime(string)
I18n.t 'models.null_date.strftime', nullable: @nullable
end
end
A final touch
Since the intention of this feature is to display copy in the view via the
localize method, we localized the response of NullDate#strftime.
In config/locales/en.yml:
en:
models:
null_date:
strftime:
No %{nullable} yet
Summary
So that works. Is it worth it? I say yes:
- It looks like a lot more code than the quick fix, but it’s quick to test-drive.
- Since the resulting Null Object is generalized, it has a high chance for re-use. In fact, it should Just Work if you drop it in your Rails app.