How to Avoid Fat Controllers and Fat Models

It’s been nearly a month since the Ruby on Rails workshop and I almost feel like it’s an entirely new year because I have been learning things that have nothing to do with Ruby programming or computers in fact. They’re all very interesting and these are things I wish I knew 10 years ago. I have also been traveling a lot, so much that I feel so disconnected with my work but I will get back soon to work and do things I usually do (in a few days).

The REAL reason why I am posting this is a need to help everyone stop making the same dumb mistakes I (and probably thousands of other developers) have done in the past. This technical debt is writing bad code which could hardly be discerned and changed by anyone including yourself after a few years. Stop and think about what you are doing. It is not about timeframe and budget and how you don’t have time to think. It is not about perfection. It is about being able to look at yourself and like what you have done.

This post is simply about “how to avoid fat controllers and fat models.”

A very good talk that explains the importance of this was done by Corey Haines on Arrrcamp.

View the video as my words are insufficient to convince you why this is so important.

Accumulating fat code, similar to getting physically fat, makes us feel bad and makes us (or someone else) spend more money and time trying to revert the mistakes.

How do we fix or avoid having fat controllers, models or generally classes in Ruby?

Use ActiveSupport Concerns

This feature has always existed in Ruby on Rails for a long time. It is an improvement to the normal way of extending a class or including instance methods through a new module. It is a fundamental concept in Ruby. See: Module.included.

For Ruby on Rails 4, one of the primary changes is the new folder called “concerns” which is meant for all the modules that use ActiveSupport::Concern.

If your model file looks like this:


class Itinerary < ActiveRecord::Base
  belongs_to :trip
  belongs_to :user

  validates :user_id, presence: true
  validates :location, presence: true

  class << self
    def for_user(user)
      self.where(user_id: user)
    end

    def for_year(date=Time.zone.now)
      self.where("travel_on > ? and travel_on < ?", date.beginning_of_year, date.end_of_year)
    end

    def total_yearly_estimated_cost(user, date=Time.zone.now)
      self.for_user(user).for_year(date).map(&:estimated_cost).reduce(:+) || 0.00
    end
  end
end

You should create a new module and make sure it is on app/models/concerns folder.


# app/models/concerns/calculate_estimated_cost.rb
module CalculateEstimatedCost

  extend ActiveSupport::Concern

  module ClassMethods
    def for_user(user)
      self.where(user_id: user)
    end

    def for_year(date=Time.zone.now)
      self.where("travel_on > ? and travel_on < ?", date.beginning_of_year, date.end_of_year)
    end

    def total_yearly_estimated_cost(user, date=Time.zone.now)
      self.for_user(user).for_year(date).map(&:estimated_cost).reduce(:+) || 0.00
    end
  end

end

Naturally, we will include that module on the model file which is a class.


# app/models/itinerary.rb
class Itinerary < ActiveRecord::Base
  belongs_to :trip
  belongs_to :user

  validates :user_id, presence: true
  validates :location, presence: true

  include CalculateEstimatedCost
end

This is the least recommended way primarily because it is a “mixin” (a class with a module included). A mixin is inheritance and we were told to avoid inheritance. This was mentioned on day one of the workshop. We need to include the module on the model file.

Use Plain Old Ruby Objects

If you’ve been doing enough Ruby or reading, you’ve probably come across similar articles or books. This idea is very old but only a few like Avdi Grimm wrote about it.

It is great way to improve controllers. This does not as simplistic as requiring a class on another class or module. You have to read more about design patterns like the use of decorators.

Does this all sound gibberish to you but you want to learn Ruby on Rails?

We came up with a decent guide for beginners. Please read the Geekcamp Baguio Ruby on Rails Workshop guide.

Further reading:

7 Ways to Decompose Fat ActiveRecord Models

Services - what are they and why we need them?

Rails Form Objects

Think About Your Architecture

Sandi Metz’ Rules for Developers

Credits:

Artiom Diomin, our overall technical lead for Assembla.