dynsne
dynsne

Reputation: 341

Ruby memoization and Null Object pattern

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

Answers (2)

3limin4t0r
3limin4t0r

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

Schwern
Schwern

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

Related Questions