Resurrecting feed_tools, part 2

by Sean Cribbs

(Follow-up on part 1)

One of the great and powerful things about Ruby is its ability to expressively extract common patterns through metaprogramming. Even the standard library in Ruby provides some good examples. One of my favorites is the generation of getter/setter methods for instance variables, the attr methods:

class Person
  attr_reader :name # adds 'name' method (getter)
  attr_writer :phone # adds 'phone=' method (setter)
  attr_accessor :address # adds both 'address' and 'address='
  attr :gender # same as attr_accessor
end

Using the metaprogramming techniques like this, we’re going to clean up a few things in feed_tools.

Memoization

I don’t know if I’d go so far as to call it an anti-pattern, but what FeedTools does in many cases is definitely not DRY. Memoization is a pattern where a value is calculated only once and thereafter stored in an instance variable so it can be reused. Here’s the basic formula the original library follows:

def some_value
  if @some_value.nil?
    # do some calculation
    @some_value = value
  end
  @some_value
end

That’s an acceptable way to do it, although stylistically I would have done a few things differently (especially using unless @some_value rather than if...nil?). However, the code is littered with these blocks. It seems every single major attribute of the Feed class has one. Let’s clean that up with some metaprogramming. First, we’re going to reduce the method just to its calculation, making sure to return the value we want to store in the ivar.

def some_value
  # do some calculation
  value
end

Now that we have distilled the essence of the calculation, let’s inject some memoization at the class level. I’d like to make an attribute-like declaration like so (Incidentally, recent versions of Rails have this already):

memoize :some_value

So, I made a module like so:

require 'activesupport' # for alias_method_chain
module FeedTools
  module Memoize
    def memoize(*names)
      names.each do |name|
        class_eval %{
          def #{name}_with_memoize
            @#{name} ||= #{name}_without_memoize
          end
        }
        alias_method_chain name, :memoize
      end
    end
  end
end

Then we’ll extend the Feed class with that module:

class FeedTools::Feed
  extend FeedTools::Memoize
end

How does it work? Let’s step through it. Since we extended the Feed class with the module, memoize becomes a class/singleton method. This means we can call it directly in our class definition. Next, when we pass it any number of symbols or strings, it will iterate through each one, defining a method with class_eval and establishing a pair of aliases. So for :some_value, it will create a method that looks like this:

def some_value_with_memoize
  @some_value ||= some_value_without_memoize
end

One thing to note about this method is that I’ve simplified the if...nil? into the simpler “or-equals” or “lazy assignment” operator. The only disadvantage to this method is if you are trying to memoize something that calculates to false, it will be recalculated everytime you call the method. Since FeedTools mostly returns strings for things, this is not an issue.

The next line after the class_eval is alias_method_chain. This is a little goodie that comes with ActiveSupport, the source of much of the magic behind Rails. Essentially, it shortens these two lines into one:

alias_method "#{name}_without_memoize", name
alias_method name, "#{name}_with_memoize"

So our “with” method is injected in the place of the original method, but leaving a hook back into the original “without” method. Incidentally, this is one example of why Ruby doesn’t need the complexities of Java-like Dependency Injection solutions — it’s a no-brainer!

In the next installment, I’ll discuss more ways to clean up the FeedTools code.

© 2006-present Sean CribbsGithub PagesTufte CSS