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.
An interactor is given a context. The context contains everything the interactor needs to do its work.
When an interactor performs its single purpose, it affects its given context.
Think of the context as a hash. As an interactor performs it can add information to the context.
context[:user] = user
When something goes wrong in your interactor, you can flag the context as failed.
context.fail!
When given a hash argument, the fail!
method can also update the context. The following are equivalent:
context[:error] = "Boom!"
context.fail!
context.fail!(error: "Boom!")
You can ask a context if it's a failure:
context.failure? # => false
context.fail!
context.failure? # => true
or if it's a success.
context.success? # => true
context.fail!
context.success? # => false
Sometimes an interactor needs to prepare its context before the interactor is even performed. This can be done by defining a setup
instance method on an interactor. The setup
method is run during interactor initialization.
def setup
context[:emails_sent] = 0
end
Your application could use an interactor to authenticate a user.
class AuthenticateUser
include Interactor
def perform
if user = User.authenticate(context[:email], context[:password])
context[:user] = user
context[:token] = user.secret_token
else
context.fail!(message: "authenticate_user.failure")
end
end
end
To define an interactor, simply create a class that includes the Interactor
module and give it a perform
instance method. The interactor can access its context
from within perform
.
The interactor also provides convenience methods for dealing with its context. The fail!
, failure?
and success?
methods on an interactor pass through to its context. Also, any key on the context is available via method call on the interactor. The interactor above could be rewritten as:
class AuthenticateUser
include Interactor
def perform
if user = User.authenticate(email, password)
context[:user] = user
context[:token] = user.secret_token
else
fail!(message: "authenticate_user.failure")
end
end
end
Most of the time, your application will use its interactors from its controllers. The following controller:
class SessionsController < ApplicationController
def create
if user = User.authenticate(session_params[:email], session_params[:password])
session[:user_token] = user.secret_token
redirect_to user
else
flash.now[:message] = "Please try again."
render :new
end
end
private
def session_params
params.require(:session).permit(:email, :password)
end
end
can be refactored to:
class SessionsController < ApplicationController
def create
interactor = AuthenticateUser.perform(session_params)
if interactor.success?
session[:user_token] = interactor.token
redirect_to root_path
else
flash.now[:message] = t(interactor.message)
render :new
end
end
private
def session_params
params.require(:session).permit(:email, :password)
end
end
The perform
class method used above is a convenience method provided by Interactor and is shorthand for:
interactor = AuthenticateUser.new(session_params)
interactor.perform
The hash argument given is converted to a context behind the scenes.
Given the user authentication example, your controller may look like:
class SessionsController < ApplicationController
def create
interactor = AuthenticateUser.perform(session_params)
if interactor.success?
session[:user_token] = interactor.token
redirect_to root_path
else
flash.now[:message] = t(interactor.message)
render :new
end
end
private
def session_params
params.require(:session).permit(:email, :password)
end
end
For such a simple use case, using an interactor actually requires more code. So why use an interactor?
We often use interactors right off the bat for all of our destructive actions (POST
, PUT
and DELETE
requests) and since we put our interactors in app/interactors
, a glance at that directory gives any developer a quick understanding of everything the application does.
▾ app/
▸ controllers/
▸ helpers/
▾ interactors/
authenticate_user.rb
cancel_account.rb
publish_post.rb
register_user.rb
remove_post.rb
▸ mailers/
▸ models/
▸ views/
TIP: Name your interactors after your business logic, not your implementation. CancelAccount
will serve you better than DestroyUser
as that interaction takes on more responsibility in the future.
SPOLIER ALERT: Your use case won't stay so simple.
In our experience, a simple task like authenticating a user will eventually take on multiple responsibilities:
- Welcoming back a user who hadn't logged in for a while
- Prompting a user to update his or her password
- Locking out a user in the case of too many failed attempts
- Sending the lock-out email notification
The list goes on, and as that list grows, so does your controller. This is how fat controllers are born.
If instead you use an interactor right away, as responsibilities are added, your controller (and its tests) change very little or not at all. Choosing the right kind of interactor can also prevent simply shifting those added responsibilities to the interactor.
There are three kinds of interactors built into the Interactor library: basic interactors, organizers and iterators.
A basic interactor is a class that includes Interactor
and defines perform
.
class AuthenticateUser
include Interactor
def perform
if user = User.authenticate(email, password)
context[:user] = user
context[:token] = user.secret_token
else
fail!(message: "authenticate_user.failure")
end
end
end
Basic interactors are the building blocks. They are your application's single-purpose units of work.
An organizer is an important variation on the basic interactor. Its single purpose is to perform other interactors.
class PlaceOrder
include Interactor::Organizer
organize CreateOrder, ChargeCard, SendThankYou
end
In the controller, you can perform the PlaceOrder
organizer just like you would any other interactor:
class OrdersController < ApplicationController
def create
interactor = PlaceOrder.perform(order_params: order_params)
if interactor.success?
redirect_to interactor.order
else
@order = interactor.order
render :new
end
end
private
def order_params
params.require(:order).permit!
end
end
The organizer passes its context to the interactors that it organizes, one at a time and in order. Each interactor may change that context before its passed along to the next interactor.
If any one of the organized interactors fails its context, the organizer stops. If the ChargeCard
interactor fails, SendThankYou
is never performed.
In addition, any interactors that had already performed are given the chance to undo themselves, in reverse order. Simply define the rollback
method on your interactors:
class CreateOrder
include Interactor
def perform
order = Order.create(order_params)
if order.persisted?
context[:order] = order
else
fail!
end
end
def rollback
order.destroy
end
end
NOTE: The interactor that fails is not rolled back. Because every interactor should have a single purpose, there should be no need to clean up after any failed interactor.
An iterator is an interactor that performs its single purpose multiple times.
An iterator receives a collection in its context and iterates over that collection, performing once for each element.
class SendInvitations
include Interactor::Iterator
collection :emails
def setup
context[:failed_emails] = []
end
def perform_each(email)
Notifier.invitation(email).deliver
rescue
context[:failed_emails] << email
end
end
Again, an iterator is performed just like any other interactor:
class InvitationsController < ApplicationController
def create
interactor = SendInvitations.perform(invitation_params)
if interactor.success?
redirect_to root_path
else
render :new
end
end
private
def invitation_params
params.require(:invitation).permit(emails: [String])
end
end
Just like an organizer, an iterator will halt and roll back if any iteration fails. The iterator works backwards through the successful iterations, passing each element into the rollback_each
method.
TIP: If you want an iterator to perform for every element in the collection, don't fail the context. Instead, just keep track of failures inside the context.
When written correctly, an interactor is easy to test because it only does one thing. Take the following interactor:
class AuthenticateUser
include Interactor
def perform
if user = User.authenticate(email, password)
context[:user] = user
context[:token] = user.secret_token
else
fail!(message: "authenticate_user.failure")
end
end
end
You can test just this interactor's single purpose and how it affects the context.
require "spec_helper"
describe AuthenticateUser do
describe "#perform" do
subject(:interactor) { AuthenticateUser.new(email: "[email protected]", password: "secret") }
context "when given valid credentials" do
let(:user) { double(:user, secret_token: "token") }
before do
expect(User).to receive(:authenticate).with("[email protected]", "secret").and_return(user)
end
it "succeeds" do
interactor.perform
expect(interactor.success?).to be_true
end
it "provides the user" do
expect {
interactor.perform
}.to change {
interactor.user
}.from(nil).to(user)
end
it "provides the user's secret token" do
expect {
interactor.perform
}.to change {
interactor.token
}.from(nil).to("token")
end
end
context "when given invalid credentials" do
before do
expect(User).to receive(:authenticate).with("[email protected]", "secret").and_return(nil)
end
it "fails" do
interactor.perform
expect(interactor.success?).to be_false
end
it "provides a failure message" do
expect {
interactor.perform
}.to change {
interactor.message
}.from(nil).to be_present
end
end
end
end
We use RSpec but the same approach applies to any testing framework.
You may notice that we stub User.authenticate
in our test rather than creating users in the database. That's because our purpose in spec/interactors/authenticate_user_spec.rb
is to test just the AuthenticateUser
interactor. The User.authenticate
method is put through its own paces in spec/models/user_spec.rb
.
It's a good idea to define your own interfaces to your models. Doing so makes it easy to draw a line between which responsibilities belong to the interactor and which to the model. The User.authenticate
method is a good, clear line. Imagine the interactor otherwise:
class AuthenticateUser
include Interactor
def perform
user = User.where(email: email).first
# Yuck!
if user && BCrypt::Password.new(user.password_digest) == password
context[:user] = user
else
fail!(message: "authenticate_user.failure")
end
end
end
It would be very difficult to test this interactor in isolation and even if you did, as soon as you change your ORM or your encryption algorithm (both model concerns), your interactors (business concerns) break.
Draw clear lines.
While it's important to test your interactors in isolation, it's just as important to write good integration or acceptance tests.
One of the pitfalls of testing in isolation is that when you stub a method, you could be hiding the fact that the method is broken, has changed or doesn't even exist.
When you write full-stack tests that tie all of the pieces together, you can be sure that your application's individual pieces are working together as expected. That becomes even more important when you add a new layer to your code like interactors.
TIP: If you track your test coverage, try for 100% coverage before integrations tests. Then keep writing integration tests until you sleep well at night.
One of the advantages of using interactors is how much they simplify controllers and their tests. Because you're testing your interactors thoroughly in isolation as well as in integration tests (right?), you can remove your business logic from your controller tests.
class SessionsController < ApplicationController
def create
interactor = AuthenticateUser.perform(session_params)
if interactor.success?
session[:user_token] = interactor.token
redirect_to root_path
else
flash.now[:message] = t(interactor.message)
render :new
end
end
private
def session_params
params.require(:session).permit(:email, :password)
end
end
require "spec_helper"
describe SessionsController do
describe "#create" do
before do
expect(AuthenticateUser).to receive(:perform).once.with(email: "[email protected]", password: "secret").and_return(interactor)
end
context "when successful" do
let(:user) { double(:user) }
let(:interactor) { double(:interactor, success?: true, user: user, token: "token") }
it "saves the user's secret token in the session" do
expect {
post :create, session: {email: "[email protected]", password: "secret"}
}.to change {
session[:user_token]
}.from(nil).to("token")
end
it "redirects to the homepage" do
response = post :create, session: {email: "[email protected]", password: "secret"}
expect(response).to redirect_to(root_path)
end
end
context "when unsuccessful" do
let(:interactor) { double(:interactor, success?: false, message: "message") }
it "sets a flash message" do
expect {
post :create, session: {email: "[email protected]", password: "secret"}
}.to change {
flash[:message]
}.from(nil).to(I18n.translate("message"))
end
it "renders the login form" do
response = post :create, session: {email: "[email protected]", password: "secret"}
expect(response).to render_template(:new)
end
end
end
end
This controller test will have to change very little during the life of the application because all of the magic happens in the interactor.