Notes on Interactors

Interactor is a sweet little gem that you can mix in to the classes implementing your business logic to expose a common interface:
class MyInteractor
  include Interactor
end

context = MyInteractor.call
=> #<Interactor::Context>
Following a conventional ‘service object’ pattern, a class method ‘call’ is defined for you. This returns a ‘context’ object, which we can query like so:
context.success?
  => true
context.failure?
  => false
You can think of ‘context’ as representing the life of the interaction – and, as we shall see, the lifespan of an interaction can be greater than that of a single interactor.
As our interactor has no behaviour, this context has had no reason to report a failure. Let’s give it some behaviour by overriding the instance method `call` to cause the interactor to fail if it is given a number that is not even.
class MyInteractor
  include Interactor
  def call
    context.fail!(error: context.value.to_s + ‘ is not an even number!’) if context.value.odd?
  end
end
context = MyInteractor.call(value: 3)
context.success?
  => false
context = MyInteractor.call(value: 4)
context.success?
  => true
One thing I’ve learned from playing with interactors so far is that calling on the `context` object all over your class quickly gets confusing. The convention I’ve adopted is to limit access to values on `context` to the `before` hook provided by interactor (alternatively you could override initialize, but using `before` feels a little more interactor-y):
class MyInteractor
  include Interactor
  before do
    @value = context.value
  end
  …
  private
  attr_reader :value
end
`Before` is run prior to the instance method `call`.
There’s a second problem with `context`, however, which is that as it is a subclass of OpenStruct you’re open to a lot of NilClass errors throughout your interactor. Misspell ‘value’ when calling `context.value`, for example, and rather than getting an easy-to-fix unknown variable error you’ll get nil silently flowing through your domain layer (in this case blowing up on `odd?`).
There are some extensions out there that provide some quasi-type-safety for your interactors, but a quick way of ensuring that the attributes you need in the `before` block are available is to convert the context to a hash and use fetch to assert the presence of the desired attributes:
  before do
    attributes = context.to_h
    @value = attributes.fetch(:value)
  end
This way, you’ll get a KeyError if a parameter is missing. You can also easily provide a default as the second argument to `fetch`.
Interactors get more interesting when you start chaining them together with `Interactor::Organizer`.  An organizer allows you to quickly compose interactors into novel sequences without having to manually wire them together. With an organizer, interactors are threaded through with a single ‘context’ object and the failure of an interactor triggers the ‘rollback’ method on those preceding it.
There are some criticisms of the Interactor gem out there:
  1. The ‘context’ variable is a glorified global variable.  The way the interactor gem encourages you to access parameters through the `context` object is potentially open to abuse. I haven’t settled on using the `before` block as a kind of constructor but it seems like some convention such as this would mitigate this danger. Notably Rack uses a very similar pattern to `context`, allowing you to pass a single “env” hash through a stack of Rack middleware.
  2. Failures are silent. Interactor doesn’t catch your errors, so I’m not sure what this criticism is about.
  3. Class names are verbs. Interactor encourages you to follow the service object pattern of encapsulating actions within classes that are named something like CreateUser or GenerateImage. This naming convention doesn’t cleave entirely to Rails’ concept of a resource, and so can be confusing. However, I don’t think this convention was ever supposed to determine the public interface of your domain layer. You could, if you choose, hide all invocations of ‘call’ behind meaningfully named methods defined on your models. So for example, you might have a `user.upgrade_account` method that is in fact implemented with a series of interactors.
Advertisements
Notes on Interactors

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s