Design Patterns in the Wild: Null Object

Josh Clayton

I knocked out a pretty decent refactoring of some of the internals of Factory Bot this past weekend. In one of my commits, I used the Null Object pattern to simplify some conditional logic that was spread across a class.

What’s the Null Object Pattern

The Null Object pattern describes the use of an object to define the concept of “null” behavior. Typically, a null object will implement a similar interface to a similar object but not actually do anything.

In the instance of Factory Bot, there’s a FactoryBot::Factory object and a FactoryBot::NullFactory object; both have a common interface by responding to the instance methods defined_traits, callbacks, attributes, compile, default_strategy, and class_name. These methods are the core of a FactoryBot::Factory and it’s important that the methods exist on the NullFactory (we’ll be calling these methods in FactoryBot::Factory).

How to Use the Pattern

In Factory Bot, the Factory object deals with taking a handful of declared attributes and running it, resulting in an instance of a class with values assigned. Factory Bot supports the concept of parent factories; attributes, callbacks, and other features get inherited, but a parent isn’t required. Here’s the code before the change; as you can see, there’s a ton of checking to see if the parent exists.

module FactoryBot
  class Factory
    def default_strategy
      @default_strategy || (parent && parent.default_strategy) || :create
    end

    def compile
      if parent
        parent.defined_traits.each {|trait| define_trait(trait) }
        parent.compile
      end
      attribute_list.ensure_compiled
    end

    protected

    def class_name
      @class_name || (parent && parent.class_name) || name
    end

    def attributes
      compile
      AttributeList.new(@name).tap do |list|
        traits.each do |trait|
          list.apply_attribute_list(trait.attributes)
        end

        list.apply_attribute_list(attribute_list)
        list.apply_attribute_list(parent.attributes) if parent
      end
    end

    def callbacks
      [traits.map(&:callbacks), @definition.callbacks].tap do |result|
        result.unshift(*parent.callbacks) if parent
      end.flatten
    end

    private

    def parent
      return unless @parent
      FactoryBot.factory_by_name(@parent)
    end
  end
end

Not only does it add extra lines of code (and every line of code is a liability), but it also forces other developers reading the code to remember if a parent exists. This context-switching across five different methods makes it hard to remember what the actual behavior of each method is doing because certain things may or may not be executed.

To use the pattern, all I did was create a NullFactory object and implement the interface I knew I needed to get rid of all the conditionals. For each method, I returned a “sensible” result; nil for class_name, default_strategy, and compile, and I delegated the remaining few methods (defined_traits, callbacks, and attributes) to definition.

module FactoryBot
  class NullFactory
    attr_reader :definition

    def initialize
      @definition = Definition.new
    end

    delegate :defined_traits, :callbacks, :attributes, :to => :definition

    def compile; end
    def default_strategy; end
    def class_name; end
  end
end

Testing is pretty straightforward since the behavior is straightforward.

describe FactoryBot::NullFactory do
  it { should delegate(:defined_traits).to(:definition) }
  it { should delegate(:callbacks).to(:definition) }
  it { should delegate(:attributes).to(:definition) }

  its(:compile)          { should be_nil }
  its(:default_strategy) { should be_nil }
  its(:class_name)       { should be_nil }
end

Now, the private instance method will always return something that behaves like a FactoryBot::Factory. Perfect.

def parent
  if @parent # the only conditional to determine if a parent exists
    FactoryBot.factory_by_name(@parent)
  else
    NullFactory.new
  end
end

Results

Here are those methods after introducing the Null Object pattern.

module FactoryBot
  class Factory
    def default_strategy
      @default_strategy || parent.default_strategy || :create
    end

    def compile
      parent.defined_traits.each {|trait| define_trait(trait) }
      parent.compile
      attribute_list.ensure_compiled
    end

    protected

    def class_name
      @class_name || parent.class_name || name
    end

    def attributes
      compile
      AttributeList.new(@name).tap do |list|
        traits.each do |trait|
          list.apply_attribute_list(trait.attributes)
        end

        list.apply_attribute_list(attribute_list)
        list.apply_attribute_list(parent.attributes)
      end
    end

    def callbacks
      [parent.callbacks, traits.map(&:callbacks), @definition.callbacks].flatten
    end

    private

    def parent
      if @parent
        FactoryBot.factory_by_name(@parent)
      else
        NullFactory.new
      end
    end
  end
end

The commit in Factory Bot can be found here. As you can see, the logic is simplified greatly across all the methods because there’s no more conditional checking. The developer reading this code doesn’t have to care if a parent is assigned or not because he can be sure that the parent, regardless of what it is, will behave in the correct manner when that method is executed.

A couple of months ago, Gabe and I implemented the same pattern in Kumade by introducing a NoopPackager.

Have you used the Null Object pattern recently? If you haven’t, your code is probably ripe for some Null Object pattern disruption!


Disclaimer:

Looking for FactoryGirl? The library was renamed in 2017. Project name history can be found here.