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

As a Rails developer I am still a bit wet behind the ears. Following the lead of a more senior developer I picked up the habit of developing all(that is four so far) applications in Rails Edge with the help of RSpec, Edge RSpec.

Last weekend I picked up a pet project which a colleague and I came up with. Nothing difficult and we figured we could get this done within the week. Boy was I wrong. Setting the whole thing up took me the better part of the day. Of course partially due to my own inexperience but the fact that I was using Edge for everything didn’t help.

I’ll skip all the things which went horribly wrong, this is what I _should_ have done:


$ rails new_prj
$ cd new_prj
$ piston import http://dev.rubyonrails.org/svn/rails/trunk vendor/rails
$ piston import svn://rubyforge.org/var/svn/rspec/trunk/rspec vendor/plugins/rspec
$ piston import svn://rubyforge.org/var/svn/rspec/trunk/rspec_on_rails vendor/plugins/rspec_on_rails
$ ./script/generate rspec

Don’t try to freeze rails using rake rails:freeze:edge. This leads to pain and the absence of a framework dependency causing the ./script/server command to fail.
Don’t try to use the current release of RSpec in combination with the edge version of Rails. This again leads to suffering and mysteriously failing tests.
Don’t forget that running ./script/generate rspec_model is not enough to get the entire RSpec framework to behave. It’ll work to some degree but has some crucial functionality missing. For example the ./script/spec command is missing.
Don’t expect Restful authentication to play nice with RSpec through Vaporbase’s spec files. Don’t get me wrong the guys of Vaporbase did a great job it just didn’t work for me. The class UserNotifier should be UserMailer in my instance and the protected helper methods in the sessions_controller_spec.rb are ran by rspec throwing weird errors. So these should be moved to the spec_helper.rb file.

As a final bonus, ever wanted to spec a single file?


$ ./script/spec spec/controllers/foo_controller_spec.rb --color -b