Arturo Herrero

Intercept-Cache-Invoke Pattern

I heard about the Intercept-Cache-Invoke pattern for the first time from Graeme Rocher; he was explaining how he had implemented the dynamic finders on Grails.

The idea is to dynamically figure out the behaviour for methods upon invocation so that we can create new methods with flexible and dynamic names on-the-fly.

A synthesized method may not exist as a separate method until we call it. When we call a nonexistent method, we can intercept the call, allow our application to implement it on the fly, let us cache that implementation for future invocation, and then invoke it. The first call takes performance hit but next calls are faster.

Bonus: Enrique García pointed out to me that it’s a good idea to define respond_to_missing? when overriding method_missing [thoughtbot].

require "benchmark/ips"

class Person
  PLAYS = %w[tennis volley basket]

  def method_missing(name, *args)
    game = name.to_s.split("play_").last
    if PLAYS.include?(game)
      "playing #{game}"
    else
      super
    end
  end
end

class PersonCached
  PLAYS = %w[tennis volley basket]

  def method_missing(name, *args)
    game = name.to_s.split("play_").last
    if PLAYS.include?(game)
      cache_and_invoke(name) do
        "playing #{game}"
      end
    else
      super
    end
  end

  def cache_and_invoke(name)
    self.class.class_eval do
      define_method(name) do
        yield
      end
    end
    yield
  end
end

Benchmark.ips do |x|
  x.report "Method Missing" do
    person = Person.new
    person.play_tennis
    person.play_tennis
  end

  x.report "Intercept-Cache-Invoke" do
    person = PersonCached.new
    person.play_tennis
    person.play_tennis
  end

  x.compare!
end

Benchmark

It benchmarks about ~2.5x to ~4.5x faster than the method missing version. The result depends on how the solution has been implemented. For example, here we are using a block to cache and invoke new methods. Blocks are slow, and their performance depends on whether we use block.call or just yield [benchmark].

Calculating -------------------------------------
        Method Missing    20.758k i/100ms
Intercept-Cache-Invoke    48.562k i/100ms
-------------------------------------------------
        Method Missing    316.730k (± 8.0%) i/s -      1.578M
Intercept-Cache-Invoke      1.010M (± 5.2%) i/s -      5.050M

Comparison:
Intercept-Cache-Invoke:  1010089.5 i/s
        Method Missing:   316729.8 i/s - 3.19x slower

March 08, 2015 | @ArturoHerrero