May 3rd, 2011 by Josh

Organise Your Models

We've recently taken on a few large projects at Stac and one thing that's always bothered us was how large your models can get. This becomes more of a problem if you follow the skinny controller, fat model rule, as you're wrapping up a lot of your business logic within model methods.

Code organisation might not be an immediate issue in Rails as it lays down some simple organisational conventions from the get-go, but that doesn't mean there's an excuse for letting things get out of hand.

One thing that works particularly well with convoluted models is wrapping up chunks of code into modules. There are a few benefits of this approach; testability, maintainability and readability.

Let's take a simple example of a user that wants to be able to add a remove friends in an application. Your User model might look something like this:

# app/models/user.rb
class User < ActiveRecord::Base
  # ... other user methods ...

  has_many :friendships, :foreign_key => :initiator_id
  has_many :friends, :through => :friendships, :source => :recipient

  def befriend(other)
    # Some way of befriending another user
    self.friends << other
  end

  def unfriend(other)
    # Some way of removing an existing friend
    self.friends.delete(other)
  end

  def has_friend?(other)
    # Some way of checking the inclusion a
    # user in another users friend list
    self.friends.include?(other)
  end

end

And your Friendship model might look something like this:

# app/models/friendship.rb
class Friendship < ActiveRecord::Base
  belong_to :initiator, :foreign_key => :initiator_id, :class_name => "User"
  belong_to :recipient, :foreign_key => :recipient_id, :class_name => "User"
end

This can be fine for a smaller project, but as it becomes more complex you might also have methods which deal with authentication, authorisation and modify news feed content or information related to their profile. What we need to do is group related methods into smaller units. It's best to be strict about these conventions earlier on to minimise the impact of refactoring against the code base later.

So how do we go about organising our code? Firstly create a file at lib/models/user/friendship_methods.rb and define a module to contain our code in like so:

module Models
  module User
    module FriendshipMethods
      extend ActiveSupport::Concern

      included do
      end

      module ClassMethods
      end

      module InstanceMethods
      end
    end
  end
end

Here we're using ActiveSupport::Concern which cleans up the convention of having class and instance methods mixed in to the including class. Anything we include within the @included@ block will be class evaluated (class_eval) on the including class.

Now we can move our friendship methods into the module:

# lib/models/user/friendship_methods.rb
module Models
  module User
    module FriendshipMethods
      extend ActiveSupport::Concern

      included do
        has_many :friendships, :foreign_key => :initiator_id
        has_many :friends, :through => :friendships, :source => :recipient
      end

      module InstanceMethods

        def befriend(other)
          # Some way of befriending another user
          self.friends << other
        end

        def unfriend(other)
          # Some way of removing an existing friend
          self.friends.delete(other)
        end

        def has_friend?(other)
          # Some way of checking the inclusion a
          # user in another users friend list
          self.friends.include?(other)
        end
      end
    end
  end
end

...and include it in our User class:

# app/models/user.rb
class User < ActiveRecord::Base
  include Models::User::FriendshipMethods

  # ... other user methods ...
end

Much better. As mentioned before one of the benefits of splitting your code up this way is how it aids unit testing. We can easily split our specs up into their relevant counterparts:

# spec/lib/models/user/friendship_methods_spec.rb
describe User, '(Friendship Methods)' do

  it "should have a has_many association on friends through friendships"
  it "should have a has_many association on friendships"

  context '#befriend' do
    it "should be able to create a friendship between another user"
    it "should do nothing if a friendship already exists"
  end

  context '#unfriend' do
    it "should be able to remove an existing friendship"
    it "should do nothing if a friendship doesn't exist"
  end

  context '#has_friend?' do
    it "should return true if a friendship exists"
    it "should return false if no friendship is found"
  end
end

As your project grows, you'll begin to have groups of well organised, well tested units. As your class grows you'll easily be able to maintain segments of the code from within their modules. Our User class could have many more modules encapsulating different functionality, but our class body remains concise:

class User < ActiveRecord::Base
  include Models::User::AuthenticationMethods
  include Models::User::AuthorisationMethods
  include Models::User::ProfileMethods
  include Models::User::FriendshipMethods
  include Models::User::FeedMethods

end

How do you organise your code?