Interactor - nifty idea, nifty library

Łukasz Kiełczykoski

https://github.com/collectiveidea/interactor I love this little nifty library which greatly encapsulates my logic and allows me to clear my Controllers. Interactors are a form of Service Objects (others may call an interactor a Use Case Object) but you don’t have to define the interfaces yourself and a way of input, a way of invalidating the action as well as a way of checking if an action was a success or not are already there for you.

There are few things that are characteristic for that kind of objects:

  • accepting input,
  • performing some action,
  • returning result.

If you decide to implement all of these three things you have quite a nut to crack and you will end up with something similar what Interactors gives you out of the box or something worse because you don’t have time for serious design. Of course, in some cases, very simple service object are enough but if your application’s business logic contains a few processes, you might consider something more powerful. That’s why I would like to show you this library because it comes with many features and yet it is light and simple.

Accepting input

Interactors accepts hash as an input which is great because it provides readable and idiomatic way of passing your data.

class UserController < ApplicationController
  def create
    result = RegisterUser.call(params: user_params)
    # response
  end

  private

  def user_params
    params.require(:user).permit(:email, :password)
  end
end

Internally the library converts it to Context object which is available inside interactor under context attribute. This is a way of communication between interactors as well as with interactor itself but that’s for later.

Performing an action

Every interactor has single method call which starts its piece of logic. Call method must return Context

Returning result

…because a context is an object which tells us if an action was performed successfully or not. Context has two methods for this - success? or failure?, depends on our preferences. By default context is successful until you fail it inside your interactor.

class RegisterUser
  include Interactor

  def call
    user = User.new(context.params)
    if user.save
      context.user = user # context.success? returns true
    else
      context.fail! # context.success? returns false
    end
  end
end

Knowing this, your controller might look like this:

class UserController < ApplicationController
  def create
    result = RegisterUser.call(params: user_params)
    if result.success?
      # 200
    else
      # 422
    end
  end

  private

  def user_params
    params.require(:user).permit(:email, :password)
  end
end

That was only simple example and frequently some processes require more steps rather than just creation. In our example we would like to send a confirmation email after we create a user. Logic for that we could put next to user creation, however this process is getting more complex and we want to keep our logic divided by its responsibility and keep it easy to test. So, we can define two steps in this registration process:

  1. creating a user,
  2. sending him a confirmation email.

I’m going to put each step in its separate interactor - CreateUser, SendConfirmationEmail.

class CreateUser
  include Interactor

  def call
    user = User.new(context.params)
    if user.save
      context.user = user # context.success? returns true
    else
      context.fail! # context.success? returns false
    end
  end
end

class SendConfirmationEmail
  include Interactor

  def call
    UserMailer.confirmation(context.user).deliver!
  end
end

Now, we need a way of running both of these interactors in order to fulfill the process of registration. However, we don’t need to create any logic ourselves.

Organizers

Library defines for us a special interactor which is able to run list of interactors in a sequential manner. That special interactor is called Organizer and in our case it would look like this:

class RegisterUser
  include Interactor::Organizer

  organize CreateUser, SendConfirmationEmail
end

Organizer call each interactor and passes context from one to the next. If any interactor fail in the middle of whole process, organizer stops and it is marked as failed as well.

Conclusion

As you can see, we expanded our registration process but our controller didn’t change and its job is only to call interactor and handle its result. Thanks to interactors, controllers stay clean, simple and easy to test.

I hope this post was informative for you and my bad English grammar didn’t make you cry.