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.
Top comments (0)