Thinner Models in Rails

Ademar Tutor | Aug 15, 2013




The Rails convention is to put your business logic should be on the models. Here’s a typical example:

# app/controllers/users_controller.rb

class UsersController < ApplicationController  
  def suspend
    @user = User.where(user_id: params[:id]).first
    @user.suspend!
    redirect_to @users, notice: 'Successfully suspended user!'
  end
end  
# app/controllers/user.rb

class User  
  include Mongoid::Document

  def suspend!
    update_attributes(banned: true)
    solutions.each { |s| s.update_attributes(hide: true) 
    profile.update_attributes(hide: true)
  end
end  

The problem with this code is that too much logic is cluttered into a single method on the model. This makes your code harder to read and maintain.

One way to refactor your code is to break suspend! into multiple methods:

# app/controllers/user.rb

class User  
  include Mongoid::Document

  def suspend!
    ban_user!
    hide_solutions!
    hide_profile!
  end

  def ban_user!
    update_attributes(banned: true)
  end

  def hide_solutions! 
    solutions.each { |s| s.update_attributes(hide: true) 
  end

  def hide_profile!
    profile.update_attributes(hide: true)
  end

end

The problem with this code is that class does too many things (too much logic inside a single class) which breaks the Single Responsibility Principle.

The best way to do this is to put the unique business logic of suspending a user on a new class ‘UserSuspension’. Take note that the new class doesn’t have to be an ActiveRecord (Mongoid Document). This class can be a PORO (Plain Old Ruby Object).

# app/models/user_suspension.rb

class UserSuspension  
  def initialize(user)
    @user = user
  end

  def create
    ban_user!
    hide_solutions!
    hide_profile!
  end

  private
    def ban_user!
      update_attributes(banned: true)
    end

    def hide_solutions! 
      solutions.each { |s| s.update_attributes(hide: true) 
    end

    def hide_profile!
      profile.update_attributes(hide: true)
    end
end 

The main advantage of putting this unique business logic on a PORO makes this class responsible for only one thing. It makes it easier to read and maintain your code.

Now on our controller, let’s see the PORO in action.

# app/controllers/users_controller.rb

class UsersController < ApplicationController  
  def suspend
    @user = User.where(user_id: params[:id]).first
    suspension = UserSuspension.new(@user)
    suspension.create!
    redirect_to @users, notice: 'Successfully suspended user!'
  end
end

Using PORO helps your Rails application become more modular and easier to maintain.