Plain Old Ruby Objects (POROs) in Rails Fat Models
During the implementation of the fat-model thin-controller phenomena, the model gets bloated with a lot of code but POROS resolves that issue.
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.
# 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
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.