K Putra
Posted on December 25, 2019
If you continue to read this article, I assume that you know Ruby, OOP in Ruby, and RoR. Perhaps this article is for beginner in rails.
Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live.
This is important. Well-structured code has many benefits, one of it is easy to maintained. In rails, the first sign of well-structured code is skinny controller, skinny model.
Let's start our journey! (I use Rails API-only as example, but this article can be implemented in normal Rails as well)
Table of Contents:
Phase 1: Fat Controller, Skinny Model
Phase 2: Skinny Controller, Fat Model
Phase 3: Skinny Controller, Skinny Model
Supplement
Phase 1: Fat Controller, Skinny Model
So, if you are new in rails, probably you just put your business logics in controllers, causing your controllers have hundreds lines of code. On the other hand your models are only have a few lines of code. This is called fat controller, skinny model.
It is not ilegal. But always remember what I quoted in the beginning of this article!
I'll use example. Let say we have these 2 controllers and 2 models:
# app/controllers/users_controller.rb
# total: 37 lines
class UsersController < ApplicationController
def create
user = User.create!(user_params)
# 10 lines of BusinessLogic-A
# 5 lines of BusinessLogic-B
render json: { status: "OK", message: "User created!" }, status: 201
end
def update
user = User.update!(user_params)
# 5 lines of BusinessLogic-B
render json: { status: "OK", message: "User updated!" }, status: 200
end
private
def user_params
params.permit(:role, :public_id, :email, :password)
end
end
# app/controllers/companies_controller.rb
# total: 80 lines
class CompaniesController < ApplicationController
def create
company = Company.create!(company_params)
# 20 lines of BusinessLogic-C
# 14 lines of BusinessLogic-D
render json: { status: "OK", message: "Company created!" }, status: 201
end
def update
company = Company.create!(company_params)
# 20 lines of BusinessLogic-C
# 9 lines of BusinessLogic-E
render json: { status: "OK", message: "Company updated!" }, status: 200
end
private
def company_params
params.permit(:name, :tax_id)
end
end
# app/models/user.rb
# total: 3 lines
class User < ApplicationRecord
has_secure_password
end
# app/models/company.rb
# total: 2 lines
class Company < ApplicationRecord
end
What is the first thing you notice? I hope it is that the controllers both have redundant code: UsersController has two BusinessLogic-B written, CompaniesController has two BusinessLogic-C written.
Well, you might get killed!
You can put, let say, BusinessLogic-C as method in CompaniesController, so it does look like this:
# app/controllers/companies_controller.rb
# total: 46 lines (from 80 lines)
class CompaniesController < ApplicationController
def create
@company = Company.create!(company_params)
business_logic_c
# 14 lines of BusinessLogic-D
render json: { status: "OK", message: "Company created!" }, status: 201
end
def update
@company = Company.create!(company_params)
business_logic_c
# 9 lines of BusinessLogic-E
render json: { status: "OK", message: "Company updated!" }, status: 200
end
private
def company_params
params.permit(:name, :tax_id)
end
def business_logic_c
# 20 lines of BusinessLogic-C
end
end
So you reduce CompaniesController from 80 lines to 46 lines. Not so bad, you have implemented DRY. But it is not good enough.
Why? Let's move to next phase.
Phase 2: Skinny Controller, Fat Model
To answer your last question, I'll quote from other:
“Fat Model, Skinny Controller” refers to how the M and C parts of MVC ideally work together. Namely, any non-response-related logic should go in the model, ideally in a nice, testable method. Meanwhile, the “skinny” controller is simply a nice interface between the view and model.
In practice, this can require a range of different types of refactoring, but it all comes down to one idea: by moving any logic that isn’t about the response to the model (instead of the controller), not only have you promoted reuse where possible but you’ve also made it possible to test your code outside of the context of a request.
So, let's move our BusinessLogic from controllers to models!
# app/controllers/users_controller.rb
# total: 20 lines (from 37 lines)
class UsersController < ApplicationController
def create
user = User.create!(user_params)
user.business_logic_a
user.business_logic_b
render json: { status: "OK", message: "User created!" }, status: 201
end
def update
user = User.update!(user_params)
user.business_logic_b
render json: { status: "OK", message: "User updated!" }, status: 200
end
...
end
# app/controllers/companies_controller.rb
# total: 21 lines (from 80 lines)
class CompaniesController < ApplicationController
def create
company = Company.create!(company_params)
company.business_logic_c
company.business_logic_d
render json: { status: "OK", message: "Company created!" }, status: 201
end
def update
company = Company.create!(company_params)
company.business_logic_c
company.business_logic_e
render json: { status: "OK", message: "Company updated!" }, status: 200
end
...
end
# app/models/user.rb
# total: 24 lines (from 3 lines)
class User < ApplicationRecord
has_secure_password
def business_logic_a
# 10 lines of BusinessLogic-A
end
def business_logic_b
# 5 lines of BusinessLogic-A
end
end
# app/models/company.rb
# total: 53 lines (from 2 lines)
class Company < ApplicationRecord
def business_logic_c
# 20 lines of BusinessLogic-C
end
def business_logic_d
# 14 lines of BusinessLogic-D
end
def business_logic_e
# 9 lines of BusinessLogic-E
end
end
As you can see, the more complex your code is, the skinnier your controller, and the fatter your model.
Note: You can use callbacks if it is suitable for your need. Callbacks are powerful tools in rails. I won't cover callbacks in this article.
And finally, we will be going to the last phase!
Phase 3: Skinny Controller, Skinny Model
"Fat Model, Skinny Controller" is a very good first step, but it doesn't scale well once your codebase starts to grow.
Let's think on the Single Responsibility of models. What is the single responsibility of models? Is it to hold business
logic? Is it to hold non-response-related logic?No. Its responsibility is to handle the persistence layer and its abstraction.
Business logic, as well as any non-response-related logic and non-persistence-related logic, should go in domain
objects.Domain objects are classes designed to have only one responsibility in the domain of the problem. Let your classes
"Scream Their Architecture" for the problems they solve.In practice, you should strive towards skinny models, skinny views and skinny controllers. The architecture of your
solution shouldn't be influenced by the framework you're choosing
In practice, you can put Domain Objects in lib
or you make app/lib
. If you put in app/lib
, so you have to know about autoloading in rails. If you put in lib
, this is worth reading
In this article, I'll just put our Domain Objects in app/lib
. You have to make this directory first. In your root folder, run:
$ mkdir app/lib
Now the directory is ready. Let's start!
# app/controllers/users_controller.rb
# total: 20 lines (from 20 lines)
class UsersController < ApplicationController
def create
user = User.create!(user_params)
BusinessLogicA.new
BusinessLogicB.new
render json: { status: "OK", message: "User created!" }, status: 201
end
def update
user = User.update!(user_params)
BusinessLogicB.new
render json: { status: "OK", message: "User updated!" }, status: 200
end
...
end
# app/controllers/companies_controller.rb
# total: 21 lines (from 21 lines)
class CompaniesController < ApplicationController
def create
company = Company.create!(company_params)
BusinessLogicC.new
BusinessLogicD.new
render json: { status: "OK", message: "Company created!" }, status: 201
end
def update
company = Company.create!(company_params)
BusinessLogicC.new
BusinessLogicE.new
render json: { status: "OK", message: "Company updated!" }, status: 200
end
...
end
# app/models/user.rb
# total: 3 lines (from 24 lines)
class User < ApplicationRecord
has_secure_password
end
# app/models/company.rb
# total: 2 lines (from 53 lines)
class Company < ApplicationRecord
end
# app/lib/business_logic_a.rb
class BusinessLogicA
# 10 lines of BusinessLogic-A
end
# app/lib/business_logic_b.rb
class BusinessLogicB
# 5 lines of BusinessLogic-B
end
# app/lib/business_logic_c.rb
class BusinessLogicC
# 20 lines of BusinessLogic-C
end
# app/lib/business_logic_d.rb
class BusinessLogicD
# 14 lines of BusinessLogic-D
end
# app/lib/business_logic_e.rb
class BusinessLogicE
# 9 lines of BusinessLogic-E
end
So you have skinny controller, skinny model, but you add 5 more files with total of 78 lines. Adding these files is necessary. Why? Read again the quoted text in this phase (read quoted text).
Supplement
This is not the end. You still has many things to learn so your code is more beautiful and easier to maintained. Some of them are:
- Ruby Design pattern: you can start from here
- Rails Design pattern: you can start from here
- Sandi Matz Rule: you can start from here
- Clean code: you can start from here
- Rails callbacks: you can start from here
- Rails concerns: you can start from here
- Ruby Metaprogramming: you can start from here
- And many more!
Thanks for reading.
Posted on December 25, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.