Perspectives

Jake1

Recently I was working with a unique relationship in which each child object was a specific instance of its parent while also inheriting multiple attributes from that parent. In this particular situation the traditional method of accessing the parents attribute though the child is both tedious and verbose. Rather then calling the attribute directly, I wanted to clean things up and call the child directly, leaving the child to fetch the attribute from the parent. Not only would this reduce the amount of code needed, but it would also help separate the concerns of the model from the views. I decided to concentrate on imitating the inheritance rather then the relationship since the inheritance evolved from normalizing the data.

At first I thought it would be easiest to override Child#method_missing, an obvious choice for dynamically monkey patching any object. Unfortunately I was unable to find a working pattern that did not rely on exceptions, the time consuming respond_to? method or a white list of method names to forward. The voices in my head kept insisting there had to be a better, more ruby-esque way to achieve the same result while automating the grunt work.

With a bit of help from Google and caffeine I stumbled over Rails Ticket 4133 incorporating the delegate method, which creates a method that delegates attribute calls onto related objects.

class Parent << ActiveRecord::Base
  has_many :children
end

class Child << ActiveRecord::Base
  belongs_to :parent
  delegate :name, :to => :parent
end

However there are a few drawbacks to using the delegate method, such as still relying on a white-list of attributes, and the helper costing more keystrokes then the original code. Even more disturbing is that the delegate helper does not generate the attribute query method. I adore uniformity in software, and if a model method Child#name exists, so should the query accessor Child#name? (at least in Rails).

Fortunately we can inject a bit of voodoo into ActiveRecord itself to extend and customize how the delegate method operates. Although I have taken the low road and created a monkey patch, with a few slight modifications and a well rounded test suite a Rails plugin could be born.

module ActsAsDelegatableTo
    def acts_as_delegatable_to(table, *exceptions)          
          local_columns = self.columns.map { |attr| attr.name }
          table_columns = eval(%{#{table.to_s.capitalize}.columns}).map { |attr| attr.name }
          table_columns.reject! { |attr| attr == 'id' or local_columns.include?(attr) or exceptions.include?(attr) }

          table_columns.each { |attr|
                  class_eval(%{delegate :#{attr}, :to => :#{table}})
                  class_eval(%{delegate :#{attr}?, :to => :#{table}})
          }
    end
  end

  ActiveRecord::Base.send :include, ActsAsDelegatableTo
  

Rather then explicitly programming or including the delegate methods using the white-list approach, acts_as_delegatable_to creates delegate methods to each of the fields defined in the parent's data model, excluding any the child may have redefined. But wait, there's more! For only a few key strokes more we also gave ourselves the standard Rails attribute query methods, providing access to not only Parent#attribute but Parent#attribute? as well.

class Parent << ActiveRecord::Base
  has_many :children
end

class Child << ActiveRecord::Base
  belongs_to :parent
  acts_as_delegatable_to :parent
end

By calling acts_as_delegatable_to :parent, each Child object instance will have attribute accessors and query methods to each field belonging to its parent. Just remember that this sort of relationship is not common before blindly inheriting all of the attributes of related objects. This method could cause nasty headaches and sleepless nights if it is not used with caution. There are no protections for naming conflicts or duplicated class evaluations, and the inheritance is generated based on the data model, not the business logic. If you decide to take this approach, just proceed with caution!




RSS Feed


CATEGORIES


ARCHIVES


BOOKMARKED


Add to Technorati Favorites