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
- Group
- Forum
- User
- Post
- 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?