Picture of stu

TWIR July 26, 2007: ActiveRecord

  • Posted By Stuart Halloway on July 26, 2007

This week in refactoring, we will make a small improvement to ActiveRecord. Along the way, we will encounter and solve the following issues:

  • methods hung from the wrong object
  • Law of Demeter violations
  • misleading comments

This refactoring began when Jason and I ran into a problem trying to alias an accessor method in Rails. This doesn't work:

class Topic < ActiveRecord::Base
  alias_method :body, :content
end

The problem is that ActiveRecord does not create read accessor methods until one of them is called. So even though content will be available for instances, it is not available to be aliased at class definition time. We can work around this by explicitly telling ActiveRecord to instantiate the accessor methods, like this:

class Topic < ActiveRecord::Base
  define_read_methods
  alias_method :body, :content
end

Unfortunately, define_read_methods is hung from the wrong object. It is an instance method, but it affects all instances of the class, and should be a class method. Let's write a unit test that anticipates the refactoring we hope to make:

# To call alias_method, the old_name method must already exist.
# ActiveRecord @read_methods do not exist until you call 
# define_read_methods, which should be a class method.
def test_define_read_method_callable_before_alias_method_during_class_definition
  assert_nothing_raised do
    c = Class.new(ActiveRecord::Base) do
      set_table_name :topics
      define_read_methods
      alias_method :body, :content
    end
  end
end

Here's the original implementation of define_read_methods:

def define_read_methods
  self.class.columns_hash.each do |name, column|
    unless respond_to_without_attributes?(name)
      if self.class.serialized_attributes[name]
        define_read_method_for_serialized_attribute(name)
      else
        define_read_method(name.to_sym, name, column)
      end
    end
    unless respond_to_without_attributes?("#{name}?")
      define_question_method(name)
    end
  end
end

The two calls to self.class violate the Law of Demeter and reinforce my instinct that this should be a class method. What about the calls to define_question_method, define_read_method, and define_read_method_for_serialized_attribute? It turns out that all of these methods should be class methods. This often happens: One method hanging from the wrong object leads to a bunch of other methods in the wrong place as well.

The call to respond_to_without_attributes? is a little more trouble. This method is used to see if readers have already been defined, so that we don't define the readers over and over again. But look what the comment suggests:

# For checking respond_to? without searching the attributes (which is faster).
alias_method :respond_to_without_attributes?, :respond_to?

The comment is dangerous. In our case respond_to_without_attributes? is not called because it is faster, but because it is semantically necessary. The slower respond_to? would give the wrong results. I rarely add or change comments, but I am proposing this change:

# For checking respond_to? without searching the attributes
# Faster and sometimes required for correct semantics, e.g.
# checking to see if attribute readers have been added for this class yet

Since we are planning to refactor all these methods to hang from the ActiveRecord::Base class, we will need some kind of instances_respond_to?, which Ruby does not provide. Here is a quick and dirty approach, marked TODO for more refactoring later:

# TODO: Rather than creating an instance to test this, could
# just reflect against #instance_methods. Written this way
# to minimize the change needed to move define_read_methods
# from instance to class method.
def self.instances_respond_to_without_attributes?(*args)
  self.new.respond_to_without_attributes?(*args)
end

Now we have a class-level define_read_methods. Of course, there are places in Rails that want this to be an instance method, so we should let instances delegate to classes:

delegate :define_read_methods, :define_read_method, :to => 'self.class'

Rails' delegate is a wonderful, underused helper. It greatly reduces the cost of obeying the Law of Demeter; I wish every platform I used had an equivalent.

With that we have all the pieces. All that is left to do is create a diff, and submit the patch back to the Rails team. You can see the complete patch at Rails Trac Ticket 9109.

Comments
  1. bryanlJuly 26, 2007 @ 04:57 PM

    Maybe I don’t understand what you are trying to accomplish here, but why couldn’t you just use #alias_attribute rather than #alias_method?

  2. Luke RedpathJuly 26, 2007 @ 05:24 PM

    Its discouraging to see this patch be marked as invalid when it clearly improves the design of the existing code. It doesn’t change the public API and does not break any existing tests so its an no-brainer as far as I can see: I’ve re-opened the ticket with a link to this article as I feel the Rails core should be encouraging patches that improve design and code quality.

  3. GarthJuly 26, 2007 @ 06:00 PM

    I’m interested knowing more about Law of Demeter. Could you explain it a little more simpler and maybe give an example?

  4. GarthJuly 26, 2007 @ 06:10 PM

    Ah, I’ve cracked open my copy of The Pragmatic Programmer and it has a good explanation there.

  5. Robby RussellJuly 26, 2007 @ 08:41 PM

    This reminds me of when I was working on acts_as_legacy. I needed to work around some ugly column naming conventions and wanted a way to work around column prefixes and awkward column names in general. It allowed you to set column_alias :foo, :bar in your models and read/write to them like normal and worked with the existing validation framework in ActiveRecord.