Monday, March 17, 2008

Single Table Inheritance With Nested Classes in Rails

There's a feature in Rails, which allows you to have several classes on the same table. You define a string column named 'type' and then inherit your new classes from the basic unit class. Pretty simple and sometimes very useful. It's called Single Table Inheritance or simply STI.

But in some cases there are might be quite a number of subclasses and you may even don't know how many of them will be. Say a simple case. You've got a class Game which presents a basic game model data-structure, and then you've got several subclasses which represents real games logic which have just the same data-structure but different behavior. The customer said I want those five games now and probable another good five later.

Ok, that's fine and probably an excellent opportunity for using the STI feature. But the thing which bothers me in the case is the big and unpredictable number of subclasses, and as for me, I would like to not have such a mess in my app/model/ directory.

A possible solution is in having nested subclasses for the Game class. In general it might look like this

class Game < ActiveRecord::Base
#........ some basic crap .........

class Klondike < Game
end

class Poker < Game
end
end

Then, when you create instances of the subclasses you'll have the 'type' attribute set properly to the name of the subclass.

>> g = Game::Poker.new
>> g
=> #<Game::Poker type: "Poker", ....>

This will work just the same way if would define a usual class like class PokerGame < Game, but this solution with nested classes allows you to organize your subclasses better. Now you can, by following the rails naming convention, create the following directories/files structure

app/
models/
game/
klondike.rb
poker.rb
game.rb

and then inside, say, the poker.rb file define your nested classes like that

class Game::Poker < Game
....
end

In such a way you'll keep your subclasses organized well and still will be able to use the STI feature.

8 comments:

BradfordW said...

After looking at this and discussing with a colleague of mine, are you trying to just create namespaces?

Nikolay V. Nemshilov said...

are you trying to just create namespaces?
Yes, kind of. The usual approach means that you create your subclasses next to the existing models. But there's another approach which might bring you some better code organization.

BradfordW said...

I follow ya...and yea, once Ruby or (maybe) your ideals of organizing "towards" namespacing start to mature it really starts to lack (Ruby, not your ideals :) ). At my employer we had come to a similar conclusion, it just kinda is how it is and we have to just deal with it. Ruby's still a great language but it certainly lacks in the namespace/organization arena. Hopefully, and I haven't researched this yet, there are some more intuitive solutions to this in Ruby 1.9. Anyway, take care...

Nikolay V. Nemshilov said...

I don't think that this is a ruby issue, I more convenience that it's a rails issue. When you having quite a big domain model it tends to become a mess if you follow the "rails way".

Anonymous said...

I ran into some issues with AR's ensure_proper_type method. It was stripping the "namespace" from my type names. To use the author's example, "Game::Poker" would simply become "Poker". This wouldn't be a problem, UNLESS you haappen to have a Poker class outside of the Game class. The whole problem is easily fixed by overriding the ensure_proper_type method in your subclasses.

Nikolay V. Nemshilov said...

The whole problem is easily fixed by overriding the ensure_proper_type method in your subclasses.

Yes, you're right. If you need to save the whole class-name in the database you may override the method. But all the relations are works correct even without it. I've checked it out. At last on Rails 2.0.2 it works for sure.

Anonymous said...

In my case, they did not all work.

I'll give a more exact example, to hopefully make the situation clearer:

app/models/
  family.rb
  invitation.rb
  invitation/
    family.rb
    friend.rb

So note that I have a Family class, as well as an Invitation::Family class.

In this case, Invitation::Family.find(x) will work just fine, however, Invitation.find(x) will not work. ActiveRecord tries to load a Family object instead of an Invitation::Family object.

Admittedly, this is an edge case, but I thought my comment might prevent a headache for someone else.

Nikolay V. Nemshilov said...

Uhr, sorry, I didn't understand that you've got two classes with the same name.

Yes, you are right. In such a case you'll have to patch it a little.