Using interactors to keep your controllers neat

Rails developers do their best to keep their controllers neat and trimmed down. You've probably heard the phrase "skinny controller, fat model" or even "skinny controller, skinny model".

The justification for smaller controllers comes down to a few things -

  1. Controllers are the entry point for several different route actions, which means that no one action should be cluttering up the file with its own logic. The controller is a common class for multiple actions.
  2. Logic can't be easily isolated and tested for every combination of input if it sits in the controller. The result is often a very long set of controller specs that's difficult to read, debug, and maintain.
  3. Generally keeping things simple, both visually and technically

Introducing Interactors

One of my personal favorite approaches to smaller controllers is the interactor design pattern, which lets you abstract away much of the core logic for any particular action.

An interactor is a simple, single-purpose object.

Interactors are used to encapsulate your application's business logic. Each interactor represents one thing that your application does.

- interactor gem README

An interactor object will run a series of steps (your business logic) and report back on whether the action was successful or not. This is perfect for abstracting away logic because your controller action doesn't have to care about the implementation, it just wants to know whether those steps were successfully executed or not.

The interactor gem noted above is one of many ways to implement this design pattern. I use it in the examples below because it's  extremely light-weight. In fact, I highly encourage you to read it yourself as it's only a few small files. If you're up to it, you could even implement it yourself if you're opposed to adding another dependency to your app.

An Example

My favorite use case to use interactors is for form submission validation.

Let's say we have a simple form that lets a user sign up for our app. They enter their first name, last name, and email address and hit "submit"

We'll want to validate a few things -

  1. First or Last name should not be blank
  2. Email should not be blank
  3. Email should not be taken by another existing user
  4. Email should be of a valid format (let's say x@y.z to keep it simple)

UsersController

Our controller action will receive the params and create the user

              
class UsersController < ApplicationController
  def create
    # Validate the form data and return if it is not correct
    # We'll come back and fill in the ???? with the call to the
    # interactor shortly.
    unless ("????")
      flash[:error] = "????"
      redirect_to(new_users_path)
      return
    end

    user = User.create!(user_params)
    flash[:success] = "Woohoo, you created an account"

    redirect_to(user_path(@ser))
  end

  private

  def user_params
    @user_params || = params.require(:user).permit(
      :first_name,
      :last_name,
      :email
     )
  end
end
              
            

The Interactor

Below is the interactor that we could write to perform the series of validations on the data. It takes the params hash from the controller as an input and runs through the series of checks, failing along the way if any particular check fails.

              
class UsersCreateActionInteractor
  include Interactor

  def call
    @params = context.params

    case
    when name_is_blank?         then handle_blank_name
    when email_is_blank?        then handle_blank_email
    when email_is_taken?        then handle_email_is_taken
    when email_format_invalid?  then handle_email_format_invalid
    else
      handle_success
    end
  end

  private

  def name_is_blank?
    @params[:first_name].blank? || @params[:last_name].blank?
  end

  def handle_blank_name
    # Note: The interactor gem supports a short-hand notation for
    # the below actions:
    #    > context.fail!(err_msg: "...")
    context.err_msg = "Please fill in a name"
    context.fail!
  end

  def email_is_blank?
    @params[:email].blank?
  end

  def handle_blank_email
    context.err_msg = "Please fill in an email"
    context.fail!
  end

  def email_is_taken?
    User.find_by_email(@params[:email]).any?
  end

  def handle_email_is_taken
    context.err_msg = "That email already exists!"
    context.fail!
  end

  def email_format_invalid?
    (@params[:email] =~ /.*@.*\..*/i).blank?
  end

  def handle_email_format_invalid
    context.err_msg = "Sorry, that doesn't look like an email address"
    context.fail!
  end

  def handle_success
    # n/a
  end
end
              
            

A few things to note here:

  1. You need to include the interactor module in any particular class to make it an interactor
  2. The #call method is invoked when the interactor is called.
  3. The context object is just a simple OpenStruct object provided to you by the interactor. (If you're not familiar with OpenStruct, it's very similar to a hash but lets you access keys with the dot operator).The context object contains the arguments passed to the interactor (in this case the params), and you can further set any arbitrary values on it that will be available even after the interactor has finished running (context.foo = "bar"). In this case we store the error message so our controller can access it and set it as the flash message.
  4. If any particular action fails, we call the #fail! method, which will halt further execution and fail immediately. If #fail! is never called, the interactor implicitly marks itself as successful.

The beauty of this format is that all these checks are fully encapsulated inside a separate class. It's also scalable to add more checks easily as your number of validations grows, without cluttering up the main controller.

Tying it together

Now that we have our interactor in place, lets go back to the controller and see how we should call it

              
class UsersController < ApplicationController
  def create
    validator = UsersCreateActionInteractor.call(params: user_params)

    unless validator.success?
      flash[:error] = validator.err_msg
      redirect_to(new_users_path)
      return
    end

    user = User.create!(user_params)
    flash[:success] = "Woohoo, you created an account"

    redirect_to(user_path(@ser))
  end

  private

  def user_params
    @user_params || = params.require(:user).permit(
      :first_name,
      :last_name,
      :email
     )
  end
end
              
            

We call our interactor using the class-level .call method (the gem itself has some logic to create an instance and invoke the #call method we defined earlier). The resulting object makes a few things available to us in the controller.

  • The #success? method, which returns true or false
  • The #failure? method, which is just the opposite of the above
  • Any keys you might have set on the context inside the controller. In our case, #err_msg is available from the controller

Now your logic in the controller is vastly simplified, and relies on outsourcing all the core validations/checks to a separate interactor which does the heavy lifting.

Testing

Hopefully you can see how writing tests for our use case has now become easier!

In the spirit of high-low testing, we can confidently and cleanly test every combination of params inputs in our interactor specs. That way the controller has to only focus on testing one passing and one failing scenario, with the confidence that the interactor will correctly succeed or fail as needed.