Interactor is a sweet little gem that you can mix in to the classes implementing your business logic to expose a common interface:
class MyInteractorinclude Interactorendcontext = 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?=> truecontext.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 MyInteractorinclude Interactordef callcontext.fail!(error: context.value.to_s + ‘ is not an even number!’) if context.value.odd?endendcontext = MyInteractor.call(value: 3)context.success?=> falsecontext = 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 MyInteractorinclude Interactorbefore do@value = context.valueend…privateattr_reader :valueend
`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 doattributes = 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:
- 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.
- Failures are silent. Interactor doesn’t catch your errors, so I’m not sure what this criticism is about.
- 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.