Active Record Model Adapters

I recently refactored some code in iNaturalist that fetches taxon names from external name providers like uBio and the Catalogue of Life. They return names and classification data that are similar but (of course) not identical to ActiveRecord models we use in iNat, so I figured I’d write adapters for them, and I thought a really smart solution would be to subclass the models themselves, simply overriding the attributes with getters that mined a private instance variable holding an XML response from one of the web services. Big mistake. ActiveRecord mixes in all kinds of magic into the models that doesn’t necessarily get passed on to child classes. I ran into all sorts of fun errors and problems until I remembered the Adapter implementation described here, which takes a much more sensible approach: don’t sublcass, and instead hold an internal copy of the adaptee, passing calls to anything you don’t want to override to the adaptee.

The only real “gotcha” is that ActiveRecord::Base overrides some of Object‘s methods, so you need to delegate those to the adaptee as well. I just delegated them all, though there may be some unforeseen consequences in this, I don’t know.

Anyway, here’s my solution: a model adapter module:

#
# Module for ActiveRecord model adapters.
#
# Usage:
#   class YourModelAdapter
#     include ModelAdapter
#     alias :your_model :adaptee # optional
#     
#     def initialize
#       @adaptee = YourModel.new # required!
#     end
#     
#     def some_attribute
#       do_something_different
#     end
#   end
#
module ModelAdapter
  attr_accessor :adaptee
 
  def method_missing(method, *args)
    if @adaptee.respond_to? method
      @adaptee.send(method, *args)
    else
      raise NoMethodError, "#{self.class} hasn't implemented #{method}"
    end
  end
 
  # Redirect calls to public methods in Object to the adaptee
  Object.methods.each do |method_name|
    define_method(method_name.to_sym) do |*args|
      @adaptee.send(method_name.to_sym, *args)
    end
  end
end

Comments are closed.