loading...
Cover image for Plain Old Ruby Objects (POROs) in Rails Fat Models

Plain Old Ruby Objects (POROs) in Rails Fat Models

sulmanweb profile image Sulman Baig Originally published at sulmanweb.com Updated on ・2 min read

There is a common phenomenon of “Fat models Thin Controllers” in MVC based frameworks like Ruby on Rails. When Implementing the phenomena, sometimes the model gets much bloated than you think and the file gets much much cluttered and even sometimes we cannot even find the required method.

The solution in Ruby on Rails is using simple ruby objects that cause the bloated methods to get single liners and resolve that fat model's problem. Also, this solution helps to remove business logic from models in separate classes.

The simple example is given below, where to check user session valid or not is transferred to a separate PORO object.

Before PORO:

# app/models/user.rb
class User < ApplicationRecord
  has_many :sessions, dependent: :destroy
  ...
  # model method to verify session of user
  def session_valid?(token)
    session = sessions.find_by(token: token)
    if session.nil?
      return "not_found"
    else
      if session.status == false
        return "late"
      elsif (session.last_used_at + Session::SESSION_TIMEOUT) >= Time.now # SESSION_TIMEOUT is a constant in Session Model
        # session model to update when session got used
        session.used!
        return "valid"
      else
        # session model to update to blocked status
        session.block!
        return "late"
      end
    end
  end
end

For PORO, create a folder anywhere in the project. I like to put it near the place of usage. Like in the case of model’s PORO I would put PORO in models folder. So, I create a new folder in app/models/users and create a file named valid.rb.

The contents will be:

# app/models/users/valid.rb
module Users
  class Valid
    # attr_reader to access without @ in class
    attr_reader :token
    attr_reader :user

    # delegate what attributes of the user to be used in class
    delegate :sessions, to: :user

    # initialize the class with token and user to be used in class
    def initialize(token, user)
      @token = token
      @user = user
    end

    # call the valid function for the user initialized
    def call
      # sessions are delegated for `user`
      session = sessions.find_by(token: token)
      if session.nil?
        return "not_found"
      else
        if session.status == false
          return "late"
        elsif (session.last_used_at + Session::SESSION_TIMEOUT) >= Time.now
          session.used!
          return "valid"
        else
          session.block!
          return "late"
        end
      end
    end
  end
end

Now the fat model User method will be resolved to:

# app/models/user.rb
class User < ApplicationRecord
  has_many :sessions, dependent: :destroy
  ...
  def session_valid?(token)
    Users::Valid.new(token, self).call
  end
end

So many liner methods become single liner and the model doesn’t get bloated.

The same PORO system can be used for controllers or other places to separate business logic.

It is important to test your method that are using PORO as this is clearly refactoring problem and refactoring issue can be better resolved when using testing suite.

Discussion

pic
Editor guide