Are you putting your business logic at correct place?
Tags: rails, ruby, webdev, beginners
Published: 2021-08-26 13:44:17

>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](https://github.com/collectiveidea/interactor). 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
```ruby
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
```ruby
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
```ruby
class ProcessTransaction
  include Interactor::Organizer

  organize CreateTransaction, UpdateUserBalance
end
```

#### app/interactors/create_transaction.rb
```ruby
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
```ruby
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
```ruby
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
```ruby
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
```ruby
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](https://github.com/AaronLasseigne/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.