Dynamically generate a class in Ruby
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. HereCacheFetcherBuilder#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 atcache_key
method definition. If defined in traditional, simple way@identifier
will be evaluated in scope ofCacheFetcherBuilder
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!