DEV Community

MetaDave ๐Ÿ‡ช๐Ÿ‡บ
MetaDave ๐Ÿ‡ช๐Ÿ‡บ

Posted on • Updated on

Ruby Nil Value Objects

TL;DR

A value object can be much more concisely defined if you dedicate a different class to handling null values. The null value class can be a Ruby singleton, so only one instance is ever instantiated, saving memory.

Problem

So value objects in Ruby are great.

But not everything has a value all of the time. Nils are everywhere, and how do you efficiently handle them?

If you get it wrong then you're going to be staring at this sort of thing:

class ISBN
  def initialize(input)
    @isbn_string = input
  end

  def gs1_prefix
    if isbn_string.nil?
      nil
    else
      isbn_string[0..2]
    end
  end

  def checksum_number
    if isbn_string.nil?
      nil
    else
      isbn_string[-1]
    end
  end

  def valid?
    if isbn_string.nil?
      false
    else
      etc
    end
  end

  delegate :nil?, to: :isbn_string
end

... or maybe you rely on empty strings giving you the right behaviour throughout ...

class ISBN
  def initialize(input)
    @isbn_string = input.to_s
  end

  def gs1_prefix
    isbn_string[0..2]
  end

  def checksum_number
    isbn_string[-1]
  end

  def valid?
    etc
  end

  def nil?
    isbn_string.empty?
  end
end

An empty string is not a nil though, so this is a convenience that is also a little bit wrong.

A Solution

One solution to consider is an explicit โ€œnil objectโ€ approach, which provides a response to everything that an instance of ISBN responds to, with the appropriate value for an ISBN that has no specified value or is missing.

As a bonus you only ever need one instance of this object, so you can use a Ruby singleton to represent it.

class ISBN
  class NilObject
    include Singleton

    def gs1_prefix
      nil
    end

    def checksum
      nil
    end

    def valid?
      false
    end

    def nil?
      true
    end
  end
end

... and your ISBN object's instance methods can be simplified to:

class ISBN
  def initialize(input)
    @isbn_string = input
  end

  def gs1_prefix
    isbn_string[0..2]
  end

  def checksum_number
    isbn_string[-1]
  end

  def valid?
    etc
  end

  def nil?
    false
  end
end

How to invoke this object? This works nicely ...

class ISBN
  def self.new(input)
    if input.nil?
      NilObject.instance 
    else
      super(input)
    end
  end

  def initialize(input)
    @isbn_string = input
  end

  etc
end

Yeah that's right, we overrode the new class method to return a different class. That's problematic if you enjoy type checking, but I don't think you should โ€“ you should perhaps care more that whatever is returned responds correctly.

Further thoughts

If that is not your style, see the initialisation methods considered here and consider:

module ISBNInitializerExtensions
  refine String do
    def to_isbn
      ISBN.new(gsub(/[^[:digit:]]/,""))
    end
  end

  refine NilClass do
    def to_isbn
      ISBN::NilObject.instance
    end
  end
end

This lets nil.to_isbn return a concisely defined singleton that has just the appropriate behaviour.

And why the #nil? method? You're going to need that for validations ... another post later.

Make sure you have code coverage of the nil object class, and that you have tested that it responds to everything it needs to respond to.

And lastly, other specialised object types can also be defined, so you might have ISBN, ISBN::NilObject, and ISBN::Invalid for complex cases where the behaviour of an object initialised with an invalid value differs from that of a valid value.

Top comments (0)