GIANT ROBOTS SMASHING INTO OTHER GIANT ROBOTS

Written by thoughtbot

Handling Associations on Null Objects

The Null Object Pattern is a great tool for removing conditionals in your code base. Rather than checking for nil or predicates about an object existing, you instead return a “null” implementation that responds to the same interface. The most common case I’ve applied this to in my Rails apps is the concept of a “Guest User”. For example:

class Guest
  def email
    ""
  end

  def admin
    false
  end
  alias_method :admin?, :admin

  def purchases
    []
  end
end

class ApplicationController < ActionController::Base
  include Clearance::Controller

  def current_user
    super || Guest.new
  end
end

This implementation can serve us quite well. Our code base expands, blissfully unaware of whether it’s dealing with a User or a Guest. As we add logic for our store front, we end up with methods like this:

class StoreListing < ActiveRecord::Base
  def included_in?(purchases)
    purchases.map(&:store_listing_id).include?(:id)
  end
end

Things will continue to work on our empty array, as long as we are only calling methods from the Enumerable module. However, we run into problems as soon as we write some code like this:

class CategoryStoreController < ApplicationController
  def index
    redirect_to(subcategory_store_path(subcategory_for_redirect))
  end

  private

  def subcategory_for_redirect
    last_purchase_in_category.subcategory || category.subcategory.first
  end

  def last_purchase_in_category
    @last_purchase_in_category ||= current_user.purchases.last_in_category(category)
  end

  def category
    @category ||= Category.find(params[:id])
  end
end

If the user isn’t logged in, this will fail with NoMethodError: undefined method 'last_in_category' for []:Array.

Rails to the Rescue!

Luckily, Rails 4.0 introduced a new method on ActiveRecord::Relation to help with exactly this situation! Relation#none is a method that will return a new instance of NullRelation.

NullRelation responds to every method that a normal instance of Relation would. As a bonus, when you call .none on one of your model classes, it will also respond to all of the class methods that a Relation for that class would have held as well! Our updated Guest class would look like this:

class Guest
  def purchases
    Purchase.none
  end
end

Once we’ve utilized Relation#none in our null objects, the rest of our code works as expected, and we can continue blissfully unaware of the existence of Guest in the rest of our codebase.

Backporting to Rails 3

For those of you who haven’t upgraded to Rails 4 yet (you probably should…), you can achieve a similar effect with this simple back-port:

class ActiveRecord::Base
  def self.none
    where("1 = 0")
  end
end

This isn’t quite the same as NullRelation. It’ll still hit the database and try to load data, and the call to none could be undone with .unscope(:where). However, for most cases it should act as a suitable polyfill until you’re able to upgrade to Rails 4.

What’s next?

If you enjoyed this article, you might also enjoy: