giant robots smashing into other giant robots

We are thoughtbot, a web design and development agency in Boston, MA.

July 9, 2007

Comments (View)

a compromise

On a sidebar in the latest edition of Agile Web Development I noticed something. It was a description of a class query method in ActiveRecord::Base called abstract_class?. Just what I was looking for.

Previously, I complained about not being able to do real behavior-based inheritance (the way inheritance should be used) in Rails. To me Student and Teacher are not subclasses of Person because they both have a name. State means nothing, it should be based on behavior.

Say in our app, we have users and companies. Both of which can have any number of addresses. In other words, they’re both addressable.

Let’s model it out:


class Addressable < ActiveRecord::Base

  has_many :addresses

end

class User < Addressable
end

class Company < Addressable
end

class Address < ActiveRecord::Base

  belongs_to :addressable

end

Now, using STI we’d need at an absolute minimum the following schema:


  addressables (id, type)

  addresses (id, addressable_id)

That is, our User and Company objects would both be stored in the addressables table.

Let’s say that the state about users and companies in our app is very different, such that storing the union of all their attributes in 1 table is ugly, inefficient and will result in a lot of nulls in each row. Instead I want separate tables for my users and companies. However, you can’t do that in Rails when it comes to inheritance.

Now apparently, if this ActiveRecord::Base class query method named #abstract_class? returns true Rails will never try to find a corresponding table for it in the database. That means Rails will assume its subclasses have their own tables.

Let’s rewrite the above example:


class Addressable < ActiveRecord::Base

  has_many :addresses

  def self.abstract_class?
    true
  end

end

class User < Addressable
end

class Company < Addressable
end

class Address < ActiveRecord::Base

  belongs_to :addressable

end

Sweet.

No wait.

That doesn’t work. Address belongs_to addressable, when we say address.addressable Rails will go looking for an addressables table and fail.

So we’re going to have to bust out polymorphic associations.

Rewrite:


class Addressable < ActiveRecord::Base

  has_many :addresses, :as => :addressable

  def self.abstract_class?
    true
  end

end

class User < Addressable
end

class Company < Addressable
end

class Address < ActiveRecord::Base

  belongs_to :addressable, :polymorphic => true

end

The schema:


   users (id, etc...)

   companies (id, etc...)

   addresses (id, addressable_id, addressable_type, etc...)

There we go.

Nice behavior-based inheritance. We get to refer to users and companies as addressables and we get to say:



  has_many :addresses, :as => :addressable

in 1 place.

The alternative, and common Rails idiom when using polymorphic associations, would not include the Addressable class, and be something along the lines of:


class User < ActiveRecord::Base

  acts_as_addressable

end

class Company < ActiveRecord::Base

  acts_as_addressable

end

class Address < ActiveRecord::Base

  belongs_to :addressable, :polymorphic => true

end

module ActsAsAddressable

  def acts_as_addressable
    self.class_eval do 
      has_many :addresses, :as => :addressable
    end
  end

end

ActiveRecord::Base.extend ActsAsAddressable

With that ActsAsAddressable module defined as a plugin in ‘vendor/plugins’. I don’t like this style because of the need to put the acts_as_addressable in each model.

And then you say, “But that’s more explicit, you look at the model, see the acts_as_addressable declaration, and know right away its addressable”.

Yes that’s true but you can get the same effect in the previous solution because the model subclasses Addressable. There’s no acts_as_addressable declaration but by subclassing Addressable you can infer the same information.

I don’t care much for the whole ‘acts_as’ naming convention either I’d rather just use plain Ruby ‘include’ and rewrite the above as:


class User < ActiveRecord::Base

  include Addressable

end

class Company < ActiveRecord::Base

  include Addressable

end

class Address < ActiveRecord::Base

  belongs_to :addressable, :polymorphic => true

end

module Addressable

  def self.included(clazz)
    clazz.class_eval do 
      has_many :addresses, :as => :addressable
    end
  end

end

And just put the Addressable module in ‘lib/addressable.rb’, no need for a plugin. The minute your users and companies become something more than just addressable, such as taggable, you’ll have to use either the ‘acts_as’ plugin style or just ‘include’ because Ruby only has single inheritance. Then the whole beauty of ActiveRecord::Base#abstract_class? is lost anyway.

However, ActiveRecord::Base#abstract_class? does finally give Rails developers the ‘Concrete table inheritance’ OR mapping pattern for inheritance relationships.

Interestingly, the sidebar was titled “What if I want straight inheritance?”

thoughtbotjc

gipoco.com is neither affiliated with the authors of this page nor responsible for its contents. This is a safe-cache copy of the original web site.