Delegation in Rails

March 24, 2011 by alex

I work for IPS Meteostar. We make software for meteorologists to use in the production of forecasts. In the world of aviation meteorology, metwatching is the practice of comparing a current weather observation to a forecast, and making adjustments to the forecast when they differ too much.

The definition of what ‘too much’ is varies from parameter to parameter (visibility, wind speed, temperature, etc). Our software allows users to define various rules like “if visibility varies by more than 0.5 miles, color- code the airport red on my map”. Each interval has a lower & upper boundary. A metwatching interval ties these lower & upper bounds to a color which should be displayed when the current value falls between those lower & upper boundaries.

Up to now, we’ve stored those rules in a configuration file. Now we need to move them to the database, and that has presented a challenge.

My code used to look like this:

class Interval
  attr_reader :lower, :upper
end

class MetwatchInterval < Interval
end

Note: None of my example code is complete. I’m only including enough to make my point about how the classes relate to each other. So, if it looks like there’s a lot missing… there is!

Now, MetwatchInterval needs to become a subclass of ActiveRecord::Base . So how to access all those Interval methods? Without multiple inheritance, something has to give.

This was my first approach:

class MetwatchInterval < ActiveRecord::Base

  def initialize
    @interval = Interval.new
  end

  def method_missing( meth, *args )
    if @interval.respond_to?( meth )
      @interval.send( meth, *args )
    else
      raise NoMethodError, "#{meth} is missing."
    end
  end

end

This works well enough, but it just feels unclear. method_missing can be a life-saver, but I feel like it’s worth avoiding when there are simpler solutions available. There’s that extra method call to process, and the stack traces are frequently less clear when method_missing is used.

Enter Rails' delegate.

class MetwatchInterval < ActiveRecord::Base

  delegate :upper, :lower, :to=>:@interval

  def initialize
    @interval = Interval.new
  end

end

The external API exposed by MetwatchInterval is the same as the method_missing version, plus I think it’s easier to read & understand. Under the hood, delegate just defines a few new methods on MetwatchInterval for me, so (internally), it looks the same as if I’d written

class MetwatchInterval < ActiveRecord::Base
  def initialize
    @interval = Interval.new
  end

  def upper( *args, &block )
    @interval.send(:upper, *args, &block )
  end

  def lower( *args, &block )
    @interval.send( :lower, *args, &block )
  end

end

Nice and simple. Not too much magic. There are plenty of times I’ll be glad to have discovered this. It makes favoring composition over inheritance easy, and that generally feels like good design practice to me.

☙ ☙ ☙