Rails is a wonderful library. It’s a framework that is opinionated and requires
some footwork on the developers part. One such issue is returning nil
when the
return type is an object. Returning nill doesn’t provide the view enough
information about what actions to take and leads to complex whack-a-mole
scenarios.
The solution to this problem is to introduce a NullObject
into the
application. There are null object libraries already available, but they are so
dead simple to write that it’s not necessary to use one. For example, it would
be trivial to declare this basic NullObject
.
class NullObject
def to_s
""
end
def method_missing(*args, &block)
self
end
end
object = NullObject.new
object.this.doesnt.exist #=> <NullObject>
This is extremely handy when query objects are used.
Maybes
Maybes are really handy. It’s similar to the #try
method that is in Rails.
user.try(:non_existent_method, NullObject.new) #=> <NullObject>
Instead, when the foreign key is not set, the method will return the
NullObject
.
class User < ActiveRecord::Base
belongs_to :subscription
def subscription
self.subscription_id? ? super : NullObject.new
end
end
This allows us to use the null object while still providing the original
ActiveRecord
functionality.
user = User.find(1)
user.subscription #=> <NullObject>
user.subscription.product #=> <NullObject>
user.subscription_id = 1
user.subscription #=> <Subscription>
user.subscription.product #=> <Product>
Falsiness
An issue that will be encountered is testing the falsiness of a NullObject
.
The following will not work:
obj = NullObject.new
unless obj
# Never executes
puts "I'm true"
end
A work around that should be used is defining nil?
on an instance of
NullObject
.
class NullObject
def to_s; ""; end
def nil?; true; end
def method_missing(*args, &block)
self
end
end
obj = NullObject.new
if obj.nil?
puts "I'm true"
end
This makes the code readable at a glance. When if some_val
is used then it can
be confusing at first and one must recall the old Perl style of programming.
In Perl nil
and null
were equivalent to FALSE
.
The final NullObject
that I ended up using looks like the following:
# app/models/null_object.rb
class NullObject
def initialize(*methods, &block); end
def to_s; ""; end
def to_str; ""; end
def to_i; 0; end
def to_f; 0.0; end
def to_c; 0.to_c; end
def to_r; 0.to_r; end
def to_a; []; end
def to_ary; []; end
def to_h; {}; end
def nil?; true; end
def present?; false; end
def empty?; true; end
def !; true; end
def blank?; true; end
# @return [NullObject]
def tap
yield(self)
self
end
# @return [NullObject]
def method_missing(*args, &block)
self
end
end
Custom Null Objects
This goes hand to hand with the “Maybes” section of this article. When trying to clean up view code conditionals, this is a great opportunity to use a NullObject specific to the model expected.
class NullSubscription < NullObject
def product
NullProduct.new
end
def active?
false
end
end
- @users.each do |user|
%tr
%td= user.email
%td= user.subscription.active? ? 'Subscribed' : 'Not Subscribed'
The benefit of returning a null object instead of nil
is that crazy if
checks wont be necessary in view code or model code. The ultimate goal of a
NullObject is to get it to behave like the object it is trying to imitate. This
adds predictability to the application and makes testing much easier.