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)