DEV Community

Akhil
Akhil

Posted on • Originally published at blog.akhilgautam.me

Are you putting your business logic at correct place?

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 the interactor needs to run as an independent unit.
  • every interactor has to implement a call 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 one organizer that will call all the interactors 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? & and context.success? can be used to verify the failure and success status.
  • in case of organizers if one of the organized interactors fails, the execution is stopped and the later interactors 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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

app/interactors/process_transaction.rb

class ProcessTransaction include Interactor::Organizer organize CreateTransaction, UpdateUserBalance end 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

✅✅ 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)