questioning one-to-ones

Jared Carroll

I constantly find myself reviewing and questioning my domain model. Recently I noticed a pattern in 2 different webapps.

Here’s the requirement from the client

The web site has groups. Each group has a forum. Users can create and comment on posts in the forum.

A quick scan of that sentence finds 5 nouns we care about

  1. Group
  2. Forum
  3. User
  4. Post
  5. Comment

Lets forget about modeling users for now and concentrate on the other 4.

class Group < ActiveRecord::Base

  has_one :forum

  def before_create
    self.forum = Forum.new
  end

end

class Forum < ActiveRecord::Base

  belongs_to :group

  class << self

    def popular
    end

    def recent
    end

  end

end

class Post < ActiveRecord::Base

  belongs_to :forum
  has_many :comments

end

class Comment < ActiveRecord::Base

  belongs_to :post

end

database

groups (id, name)
forums (id, group_id)
posts (id, title, body, forum_id)
comments (id, body, post_id)

Thats a real simple schema (im still ignoring users for the time being as well so no user_id in any table yet).

Looking at the requirement, finding the nouns and then creating classes for each noun is a really simple modeling technique. In this instance it yielded 4 classes (actually 5 but were ignoring users for now).

Now if an object has state usually I want to record this state in a table in the db. Looking at the schema for that forums table shows nothing but db keys, no state at all. I don’t want tables in my db that are unnecessary so lets try to get rid of it.

Looking back at our Forum model we see that it does have some useful behavior

class Forum < ActiveRecord::Base

  belongs_to :group

  class << self

    def popular
    end

    def recent
    end

  end

end

It has 2 class side finders #popular and #recent. I want to keep those. In ruby when we have behavior and no state we don’t use classes, we use modules.

A forum is a discussion so we’ll name our module along those lines

module Discussable

  def self.included(clazz)
    clazz.has_many :posts
    clazz.extend ClassMethods
  end

  module ClassMethods

    def popular
    end

    def recent
    end

  end

end

Now let’s mix this into our Group model and look at the domain model now

class Group < ActiveRecord::Base

  include Discussable

end

class Post < ActiveRecord::Base

  belongs_to :group
  has_many :comments

end

class Comment < ActiveRecord::Base

  belongs_to :post

end

We’ve eliminated the Forum class and made a Post belong_to a Group instead. The interesting behavior that was in our old Forum class now resides in the Discussable module which is then mixed into Group.

Let’s look at some usage now.

Before we’d go

Forum.popular
Forum.recent

Now we do

Group.popular
Group.recent

That’s going to be a problem once groups can be considered popular and recent by means other than the activity in their forums. That kind of makes me question eliminating the Forum class. I guess when that problem arises we could rename those methods #popular_forums and #recent_forums. In Ruby it is more accurate to model behavior only classes using modules. Once you need state with your behavior then its time to use a class.

Classes like Forum are what I like to call container/collection classes. These classes have no state and are used solely for managing a collection of other objects, in the example above that would be a forum and its posts. They usually arise from over-modeling but sometimes they contain useful behavior. Question these classes and see if you can eliminate them.

After some ask.com'ing i found some database design articles saying one to one associations were indications of a poor design.

Thoughts?