Article Image
read

What is an Interactor

My team has started using the Interactor gem to encapsulate complex business logic hence, freeing up the controllers. The Interactor gem presents us with a very nice way of representing one thing that the application does and allowing us to adhere to the Single Responsibility Principle. Also, if there is a series of action that are required by the business logic, we are able to organise a sequence of interactors using the Organizer method.

I believe that it would be easier to demonstrate the functionalities of the Interactor gem through code, of course. For a working app, pull from this github repo to view the working code. The app is built using the rails-api gem which is a lightweight rails app minus all the unnecessary view modules.

In this app, I would want to create a customer record and associate it to my user_id which is my sales_rep id. I would create an interactor that would FindSalesRep and the user_id would be extracted from the params in the JSON document.

Constructing an Interactor

A context contains everything that an Interactor needs to work with. The params from the controller sets the context when an Interactor is called. Creating an interactor is easy. Just create a class that includes Interactor and define an instance method call, whereby the context can be accessed within it.

class FindSalesRep
  include Interactor

  def call
    begin
      result = SalesRep.find(context.user_id)
      context.sales_rep_id = result.id
    rescue => e
      context.fail!(message: e)
    end
  end
end

Working with a Context

As an Interactor is run, you may add information to the context.

  context.sales_rep_id = result.id

The salesrepid can be accessed by the controller that runs the Interactor. ruby result = FindSalesRep.call(params) puts "Sales rep id #{result.sales_rep_id}" To set a failing context when an operation has failed, a context.fail! can be flagged. This can be accessed by the controller to render a flash message.

  result = FindSalesRep.call(params)
  unless result.success?
    flash.now[:message] = result.message
    render :error
  end

Organizing multiple Interactors

I find the organizer function, which is a variation of the basic Interactor, very helpful in explicitly defining the multiple responsibilities require by the controller when processing an action. The organizer is designed to run multiple Interactors in one Interactor and contents in the context is passed to every Interactor that the organizer organizes. Each Interactor may change the context by adding on more information into it to be passed on to the next Interactor. If one Interactor fails its context, the organizer would not call the subsequent Interactors.

The code will provide more clarity on this concept.

client_controller.rb

class ClientsController < ApplicationController

  def create
    res = AssociateSalesRepToClient.call(params)
    if res.success?
      render json: res.client
    else
      render json: res.message.to_s, status: 404
    end
  end
end

associate_sales_rep_to_client.rb

class AssociateSalesRepToClient
  include Interactor::Organizer

  organize FindSalesRep, CreateClient

end

find_sales_rep.rb

class FindSalesRep
  include Interactor

  def call
    begin
      result = SalesRep.find(context.user_id)
      context.sales_rep_id = result.id
    rescue => e
      context.fail!(message: e)
    end
  end
end

create_client.rb

class CreateClient
  include Interactor

  def call
    begin
      result = Client.create(sales_rep_id: context.sales_rep_id, name: context.name, abn: context.abn)
      context.client = result
    rescue => e
      context.fail!(message: e)
    end
  end
end

In the example above, the organizer AssociateSalesRepToClient defines the process of creating a Client. The task of creating a Client record requires a sales rep id to be retrieved and saved in a Client record. This example may be contrived, but it would be sufficient to demonstrate the concept of organizers.

The create will run AssociateSalesRepToClient Interactor whereby user_id is a key in the params. The FindSalesRep Interactor will run and that would set sales_rep_id into the context to be passed onto CreateClient Interactor. The Client would then be created and save if all runs well. If FindSalesRep runs into an error(user id not found), CreateClient Interactor will not be run and the context will be failed. The controller will receive a false context.success? which would proceed to render the error message.

Conclusion

Interactors are a nice way to reuse operations and explicitly specifying a sequence of actions in order to execute business logic. It also provides a way to bubble back errors when they occur in any of the operation. If in any case the error needs to rollback an operation, a rollback method can be specified in the Interactor. This serves as an option (compared to service objects and use cases) to trim down the controllers by moving business logic out of it.

Blog Logo

Daphne Rouw


Published

Image

Chronicles of a Ruby Developer

Back to Overview