Last Updated: February 25, 2016
·
874
· davidmn2

Ruby - Memoization proxy class

I've needed to create a proxy class that would cache the method results from an object because I couldn't change its implementation.

It had a lot of expensive operations that would be done for every method call, and some of these heavy methods would be called internally, and that's why I needed to come with an "intrusive" solution.

It's important to note that you probably want to avoid using something like this, you probably will always prefer to design your code better over to using this hack. Also, it's needless to say that this will give you problems in case your methods are not referentially transparent or produce side effects.

class MethodCacheProxy

  instance_methods.each { |m| undef_method m unless m =~ /(^__|^send$|^object_id$)/ }

  attr_reader :target, :cache, :invasive

  def initialize(target, invasive = false)
    @target = target
    @cache = {}
    @invasive = invasive
    intercept_instance_methods if invasive
  end

  def method_missing(name, *args, &block)
    cache_for(name, *args, &block)
  end

  def cache_for(name, *args, &block)
    unless block_given?
      cache[cache_key_for(name, args)] ||= target.send(name, *args)
    else
      target.send(name, *args, &block)
    end
  end

  def cache_key_for(name, args)
    "#{name}_#{args.hash}"
  end

  private

  def intercept_instance_methods
    instance_methods.each do |name|
      override_method(name)
    end
  end

  def instance_methods
    target.class.instance_methods(false)
  end

  def instance_method(name)
    target.class.instance_method(name)
  end

  def override_method(name)
    cacher = self
    method = instance_method(name)

    target.define_singleton_method(name) do |*args, &block|
      unless block.present?
        cache_key = cacher.cache_key_for(name, args)
        cacher.cache[cache_key] ||= method.bind(self).(*args, &block)
      else
        method.bind(self).(*args, &block)
      end
    end
  end
end