Blog

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? Let us know in the comments.

Posted on May 3rd, 2011 by Josh

Comments