This blog is a continuation of the last one where we built an
expense manager
application 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
interactor
should follow SRP(single responsibility principle). -
interactor
is provided with a context which contains everything that theinteractor
needs to run as an independent unit. - every
interactor
has to implement acall
method which will be exposed to the external world. - if the business logic is composed of several independent steps, it can have multiple
interactors
and oneorganizer
that will call all theinteractors
serially in the order they are written. -
context.something = value
can 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
organizers
if one of theorganized interactors
fails, the execution is stopped and the laterinteractors
are 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)