Several solutions for calling a method only once per instance

One of the things that I’m still getting used to in ruby is that there are many different ways to accomplish the same task.

The problem: We want our class to call an expensive method only once per instance.

Solution 1: ||= operator

We use an instance variable along with the   = operator:
class Product
  def name
    @name ||= ExternalService.expensive_call_for_name(@id)
  end
end

Now, we can do

p = Product.new
p.name
p.name

The ExternalService.expensive_call_for_name is called only once, even though we call name twice.

There is a subtle bug in this solution, however. If the expensive_call_for_name returns nil, then the expensive call will be made again due to the way ||= works. This is not the desired behavior. We want to cache the value of name, even if it is nil.

Solution 2: once method

We use a once method similar to date.rb:

class Product

  def once(*ids) # :nodoc:
    for id in ids
      module_eval <<-"end;"
        alias_method :__#{id.to_i}__, :#{id.to_s}
        private :__#{id.to_i}__
        def #{id.to_s}(*args, &block)
          (@__#{id.to_i}__ ||= [__#{id.to_i}__(*args, &block)])[0]
        end
      end;
    end
  end

  def name
    ExternalService.expensive_call_for_name(@id)
  end
  once :name
end

This solution does not have the nil limitation of solution 1. However, it is a bit heavy weight for one time problems. If there are multiple methods that should only be called once, then I would recommend this approach. If not, I believe there are simpler solutions.

Solution 3: instance_variables check

We can modify solution 1 so it works in the nil case. We will remove the use of ||= and replace it with an instance variable check:

class Product
  def name
    if !self.instance_variables.include? "@name"
      @name = ExternalService.expensive_call_for_name(@id)
    end
    @name
  end
end

This solution works as well, but it feels a bit hackish to me. I don’t love the idea of searching the instance variables to see if something is defined. Let’s try one more solution.

Solution 4: metaprogramming

We will use a bit of metaprogramming:

class Product
  def name
    expensive_name = ExternalService.expensive_call_for_name(@id)
      class<
In this case, we add a method with the same name to the singleton class of the instance of Product when name is called.  Now, the next time name is called on our instance, it will find the method on the singleton class first which uses the cached value.
If you are doing any metaprogramming, it is useful to pull
class<
 into its own method on Object.

class Object
  def singleton_class
    class<