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.

Discussion (0)