---
title: 'Rails Refactoring Example: Introduce Null Object'
teaser:
tags: web,rails,good code
author: Dan Croak
published_on: 2012-04-11
---

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.

![Smoking duck](http://media.tumblr.com/tumblr_m2bpddPXGO1qz5x9p.jpg)

## 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](http://en.wikipedia.org/wiki/Null_Object_pattern) 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](http://en.wikipedia.org/wiki/Law_of_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](http://xunitpatterns.com/Four%20Phase%20Test.html) 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`](https://github.com/svenfuchs/i18n/blob/master/lib/i18n/backend/base.rb#L48-49).
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.
