Hello Rubyist out there,
Was wondering if it's possible to use Ruby's memoization operator ||= (i.e: a || a = b when writing a ||= b) could be used on custom plain old ruby classes that are supposed to follow the null object patern.
For example, say I have a class like:
class NoThing
def status
:cancelled
end
def expires_on
0.days.from_now
end
def gateway
""
end
end
Which I use in the absence of a class Thing. Thing has the same status, expires_on, and gateway methods in it's public interface.
The question is, how can I write something like @thing ||= Thing.new in the case where @thing is either nil or NoThing ?
You could maybe crib from FalseClass and set the same operator methods on NoThing. But I'd hesitate to do so for a number of reasons.
Unlike some other languages, Ruby is very clear that there are a very limited set of things which are false, false and nil. Messing with that will probably lead to confusion and bugs down the road, it's probably not worth the bit of convenience you're looking for.
Furthermore, the Null Object Pattern is about returning an object that has the same interface as an object which does something, but it does nothing. Making it appear false would defeat that. The desire to write @thing ||= Thing.new clashes with the desire for a Null Object. You always want @thing set even if Thing.new returns NoThing, that's what Null Objects are for. The code using the class doesn't care if it's using Thing or NoThing.
Instead, for those cases when you want to distinguish between Thing and NoThing, I'd suggest having little method, for example #nothing?. Then set Thing#nothing? to return false and NoThing#nothing? to return true. This allows you to distinguish between them by asking rather than piercing encapsulation by hard coding class names.
class NoThing
def status
:cancelled
end
def expires_on
0.days.from_now
end
def gateway
""
end
def nothing?
true
end
end
class Thing
attr_accessor :status, :expires_on, :gateway
def initialize(args={})
@status = args[:status]
@expires_on = args[:expires_on]
@gateway = args[:gateway]
end
def nothing?
false
end
end
Furthermore, it's bad form for Thing.new to return anything but a Thing. This adds an extra complication to what should be a simple constructor. It shouldn't even return nil, it should throw an exception instead.
Instead, use the Factory Pattern to keep Thing and NoThing pure and simple. Put the work to decide whether to return a Thing or NoThing in a ThingBuilder or ThingFactory. Then you call ThingFactory.new_thing to get a Thing or NoThing.
class ThingFactory
def self.new_thing(arg)
# Just something arbitrary for example
if arg > 5
return Thing.new(
status: :allgood,
expires_on: Time.now + 12345,
gateway: :somewhere
)
else
return NoThing.new
end
end
end
puts ThingFactory.new_thing(4).nothing? # true
puts ThingFactory.new_thing(6).nothing? # false
Then, if you really need it, the factory can also have a separate class method that returns nil instead of NoThing allowing for @thing ||= ThingFactory.new_thing_or_nil. But you shouldn't need it because that's what the Null Object Pattern is for. If you really do need it, use #nothing? and the ternary operator.
thing = ThingFactory.new_thing(args)
@thing = thing.nothing? ? some_default : thing
In Schwerns answer already explains why you should not try to write a custom falsy class. I just want to pass you a quick and relatively simple alternative.
You could add a method on both Thing and NoThing that evaluates the instance:
class Thing
def thing?
true
end
end
class NoThing
def thing?
false
end
end
Now you can assign @thing in the following way:
@thing = Thing.new unless @thing&.thing?
This assumes @thing has always either NilClass, Thing or NoThing class.
Alternatively you can also override the Object#itself method in NoThing. This however could produce unwanted results if used by people who don't expect the differing result of #itself.
class NoThing
def itself
nil
end
end
@thing = @thing.itself || Thing.new
# or
@thing.itself || @thing = Thing.new # exact ||= mimic
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With