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<<self;self;end.class_eval do define_method :name, lambda {expensive_name} end expensive_name end end
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<<self;self;endinto its own method on Object.
class Object def singleton_class class<<self;self;end end end class Product def name expensive_name = ExternalService.expensive_call_for_name(@id) singleton_class.class_eval do define_method :name, lambda {expensive_name} end expensive_name end end
One Response to “Several solutions for calling a method only once per instance”
Sorry, the comment form is closed at this time.




TMTOWTDI at it’s best
(For the un-Perl initiated, There’s More Than One Way To Do It. Pronounced tim-toady)