Reputation: 341
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
?
Upvotes: 3
Views: 239
Reputation: 21130
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
Upvotes: 2
Reputation: 164919
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
Upvotes: 2