Ruby is still my favourite language of choice. I love it for its dynamic and elastic nature. Some time ago I wrote small bit of code that demonstrates metaprogramming capabilities of Ruby.

The issue was simple — cache airline and airport data fetched from other service to limit number for HTTP calls for data that are almost immutable. Rails framework supports caching very nice for couple of years now.

I was lucky because someone before me implemented both Airline and Airport class in the way that they respond to find(identifier) method. Just like regular ActiveRecord models. So convenient! So immediately an idea came to my head which I turned into code:

class AirportFetcher
  def initialize(airport_identifier)
    @airport_identifier = airport_identifier
  end

  def fetch
    Rails.cache.fetch("airport_#{@airport_identifier}") do
      Airport.find(@airport_identifier)
    end
  end
end

class AirlineFetcher
  def initialize(airline_identifier)
    @airline_identifier = airline_identifier
  end

  def fetch
    Rails.cache.fetch("airline_#{@airline_identifier}") do
      Airline.find(@airline_identifier)
    end
  end
end

Not too much code, but a lot of duplication. In fact one and only difference between those classes is airport vs airline. Lets do this in one class that will be able to work with both resources.

class Fetcher
  def initialize(klass, identifier)
    @klass = klass
    @identifier = identifier
  end

  def fetch
    Rails.cache.fetch(cache_key) do
      @klass.find(@identifier)
    end
  end

  private

  def cache_key
    "#{@klass.name}_#{@identifier}"
  end
end

Not so bad, now I have one universal class for fetching resources from cache. But I was not convinced to this very much. I liked previous idea with fetcher class for each resource class, but didn’t like the duplication this solution introduced. Ruby dynamism to the rescue!

class CacheFetcherBuilder
  def initialize(klass, fetch_method, expires_in: 1.day)
    @klass = klass
    @fetch_method = fetch_method
    @expires_in = expires_in
  end

  def build
    ->(klass, fetch_method, expires_in) do
      Class.new do
        define_method :initialize do |identifier|
          @identifier = identifier
        end

        define_method :fetch do
          Rails.cache.fetch(cache_key, expires_in: expires_in) do
            klass.public_send(fetch_method, @identifier)
          end
        end

        private

        define_method :cache_key do
          klass.name << "_" << @identifier.to_s
        end
      end
    end.call(@klass, @fetch_method, @expires_in)
  end
end

AirlineFetcher = CacheFetcherBuilder.new(Airline, :find).build
AirportFetcher = CacheFetcherBuilder.new(Airport, :find).build

Idea behind this class builder (sounds almost like Java) is

  • Class.new is one of the ways to define a class in Ruby. Here CacheFetcherBuilder#build will return anonymouse class which I can assign to constant.
  • Everything is wrapped into lambda with combo of define_method. Otherwise instance var of anonymouse class will be evaluated to fast. Look at cache_key method definition. If defined in traditional, simple way @identifier will be evaluated in scope of CacheFetcherBuilder instance.

Of course this is couple lines of code more in second solution, but now I have two fetcher classes without code duplication. Neither by inheritance nor by mixin. Perfect!