This blog is a continuation of the last one where we built an
expense managerapplication with business logic scattered in the controller.
Design pattern
Design pattern is a set of rules that encourage us to arrange our code in a way that makes it more readable and well structured. It not only helps new developers onboard smoothly but also helps to find bugs. 🐛
In Rails' world, there are a lot of design patterns followed like Service Objects, Form Objects, Decorator, Interactor, and a lot more.
Interactor
In this blog, we are going to look at Interactor using interactor gem. It is quite easy to integrate into an existing project.
- every
interactorshould follow SRP(single responsibility principle). -
interactoris provided with a context which contains everything that theinteractorneeds to run as an independent unit. - every
interactorhas to implement acallmethod which will be exposed to the external world. - if the business logic is composed of several independent steps, it can have multiple
interactorsand oneorganizerthat will call all theinteractorsserially in the order they are written. -
context.something = valuecan be used to set something in the context. -
context.fail!makes the interactor cease execution. -
context.failure?& andcontext.success?can be used to verify the failure and success status. - in case of
organizersif one of theorganized interactorsfails, the execution is stopped and the laterinteractorsare not executed at all.
Let's refactor our expense manager
We can create interactors for the following:
- create user
- authenticate user
- process a transaction
- create a transaction record
- update user's balance
Create a directory named interactors under app to keep the interactors.
app/interactors/create_user.rb
class CreateUser
include Interactor
def call
user = User.new(context.create_params)
user.auth_token = SecureRandom.hex
if user.save
context.message = 'User created successfully!'
else
context.fail!(error: user.errors.full_messages.join(' and '))
end
end
end
app/interactors/authenticate_user.rb
class AuthenticateUser
include Interactor
def call
user = User.find_by(email: context.email)
if user.authenticate(context.password)
context.user = user
context.token = user.auth_token
else
context.fail!(message: "Email & Password did not match.")
end
end
end
app/interactors/process_transaction.rb
class ProcessTransaction
include Interactor::Organizer
organize CreateTransaction, UpdateUserBalance
end
app/interactors/create_transaction.rb
class CreateTransaction
include Interactor
def call
current_user = context.user
user_transaction = current_user.user_transactions.build(context.params)
if user_transaction.save
context.transaction = user_transaction
else
context.fail!(error: user_transaction.errors.full_messages.join(' and '))
end
end
end
app/interactors/update_user_balance.rb
class UpdateUserBalance
include Interactor
def call
transaction = context.transaction
current_user = context.user
existing_balance = current_user.balance
if context.transaction.debit?
current_user.update(balance: existing_balance - transaction.amount)
else
current_user.update(balance: existing_balance + transaction.amount)
end
end
end
app/interactors/fetch_transactions.rb
class FetchTransactions
include Interactor
def call
user = context.user
params = context.params
transactions = user.user_transactions
if params[:filters]
start_date = params[:filters][:start_date] && DateTime.strptime(params[:filters][:start_date], '%d-%m-%Y')
end_date = params[:filters][:end_date] && DateTime.strptime(params[:filters][:end_date], '%d-%m-%Y')
context.transactions = transactions.where(created_at: start_date..end_date)
else
context.transactions = transactions
end
end
end
Let's now refactor our controllers to use the above interactors.
app/controllers/users_controller.rb
class UsersController < ApplicationController
skip_before_action :verify_user?
# POST /users
def create
result = CreateUser.call(create_params: user_params)
if result.success?
render json: { message: result.message }, status: :created
else
render json: { message: result.error }, status: :unprocessable_entity
end
end
def balance
render json: { balance: current_user.balance }, status: :ok
end
def login
result = AuthenticateUser.call(login_params)
if result.success?
render json: { auth_token: result.token }, status: :ok
else
render json: { message: result.message }, status: :unprocessable_entity
end
end
private
def user_params
params.require(:user).permit(:name, :email, :password, :balance)
end
def login_params
params.require(:user).permit(:email, :password)
end
end
app/controllers/user_transactions_controller.rb
class UserTransactionsController < ApplicationController
before_action :set_user_transaction, only: [:show]
def index
result = FetchTransactions.call(params: params, user: current_user)
render json: result.transactions, status: :ok
end
def show
render json: @user_transaction
end
def create
result = ProcessTransaction.call(params: user_transaction_params, user: current_user)
if result.success?
render json: result.transaction, status: :created
else
render json: { message: result.error }, status: :unprocessable_entity
end
end
private
def set_user_transaction
@user_transaction = current_user.user_transactions.where(id: params[:id]).first
end
def user_transaction_params
params.require(:user_transaction).permit(:amount, :details, :transaction_type)
end
end
✅✅ That is it. Our controllers look much cleaner. Even if someone looks at the project for the first time, they will know where to find the business logic. Let's go through some of the pros & cons of the interactor gem.
Pros 👍
- easy to integrate
- straightforward DSL(domain-specific language)
- organizers help follow the SRP(single responsibility principle)
Cons 👎
- argument/contract validation not available
- the gem looks dead, no active maintainers
That is it for this blog. It is hard to cover more than one design pattern in one blog. In the next one, we will see how we can use active_interaction and achieve a much better result by extracting the validations out of the models.
Thanks for reading. Do share your suggestions in the comments down below.
Top comments (0)