Archive for December, 2007

Modules, inheritance and classes

Wednesday, December 12th, 2007

I can be a bit of a purist when it comes to programming. I love to have my code DRY and go to lengths to achieve that. Today I wrote a piece of code which would enable the duplication of Rails models. This in order to support some templating. Well it’s a little bit more than ’some’ as I need to copy the entire model with all associated models(and their associated models, and their …). To keep things flexible I did not wanted to loop over a couple predefined fields and do all the copying there:


copy = Project.new
p = Project.find(:first)
copy.title = p.title
copy.description = p.description
p.members.each do |member|
  copy.members.build(:name => member.name, :function => member.function)
end

This is prone to errors not to mention ugly(and a lot of dumb work).

So I figured I’d write something better. This was quickly done:


class Project < AR:B
  cattr_accessor :non_duplicatable_columns
  non_duplicatable_columns = [primary_key, 'created_at', 'updated_at','created_on', 'updated_on'] + column_names.select {|column_name| column_name =~ /_id$/}      

  def copy
    copy_columns = self.class.column_names - self.class.non_duplicatable_columns
    a = Hash.new
    copy_columns.each {|cc| a[cc] = self.send(cc) }
    clone = Project.create(a)
  end
end

But I needed this piece of functionality in several models. So what’s a good programmer to do? Refactor the code into a module of course! But there I hit some snags. The method cattr_accessor, for example, is a bit of a strange beast. It is not documented but is should be read like ‘class attribute accessor’. The method creates class instance methods. This is something different from instance methods which you usually use. Instance methods are callable on an object which is instance of a class.


p = Project.new #class instance method
p.copy #instance method

Defining class instance methods goes with the

self

keyword. It reminded me of the Java static keyword.


def self.foo
  #do something, can be called as: FullClassName.foo
end

Keep the eye on the ball here, self points to the object on which this method is called. When defining the copy method the method column_names is called but this is a class instance method from the class subclassing AR:B. So the objects looks at itself, asks who the fathering class is and calls the desired method from that class.

The method

cattr_accessor

can no longer float freely in the class definition. It needs a container as the module is read at the moment the Rails stack is initialize and at that point the module is not necessarily hooked into a class in which a cattr_accessor makes sense. Luckily the module “http://ruby-doc.org/core/classes/Module.html”:Module defines a method included which is called whenever the module is, well, included. Using this we can still define the

non_duplicatable_columns

on a per model basis.

Finalized the module looks like this(stored in lib/duplicatable.rb):


module Duplicatable

  def Duplicatable.included(base)
    base.cattr_accessor :non_duplicatable_columns
    base.non_duplicatable_columns = [base.primary_key, 'created_at', 'updated_at','created_on', 'updated_on'] + base.column_names.select {|column_name| column_name =~ /_id$/}
  end

  def copy
    copy_columns = self.class.column_names - self.class.non_duplicatable_columns
    a = Hash.new
    copy_columns.each {|cc| a[cc] = self.send(cc) }
    clone = self.class.create(a)
  end
end

And a simple include Duplicatable adds all its functionality to a model.
Why not use a super class here? That would require calling a initialize function(to set the non_duplicatable_columns) in each model with would extend this super class. Which would result in more code and thus more places where this can break.

Living on the Edge

Monday, December 10th, 2007