Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ruby memoization and Null Object pattern

Tags:

ruby

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 ?

like image 940
dynsne Avatar asked May 02 '26 11:05

dynsne


2 Answers

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
like image 76
Schwern Avatar answered May 04 '26 20:05

Schwern


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
like image 37
3limin4t0r Avatar answered May 04 '26 19:05

3limin4t0r



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!