It is based on the Command pattern, where each Command class/object represents a task and has one public method. Basically, something like:
PressButton.new(button).execute # or
PurchaseOrder.new(params).call # or even
Song::Create.(params) # or a proc
SendEmail.perform(email) # it could be a class method, why not?
In order to get more control, we encourage to follow the next guidelines
class BaseInteractor
include Interactor
include ActiveModel::Validations
# For any class that inherits this class, a `before` hook is registered, which raises ArgumentError if the reqired parameters are not passed in during invocation.
def self.inherited(subclass)
subclass.class_eval do
def self.requires(*attributes)
validates_each attributes do |record, attr_name, value| #from ActiveModel::Validations
if value.nil?
raise ArgumentError.new("Required attribute #{attr_name} is missing")
end
end
delegate *attributes, to: :context
end
before do
context.fail!(errors: errors) unless valid? # runs every validation
end
def read_attribute_for_validation(method_name)
context.public_send(method_name)
end
end
end
end
class MyInteractor < BaseInteractor
requires :email, :name
validate :email_is_internal
def call
# ...
end
private
def email_is_internal
return unless email.present?
errors.add(:email, 'domain should be yotepresto.com') unless context.email.match(/^[a-z]+.\@yoteprseto\.com$/i)
end
end
result = MyInteractor.call(email: '[email protected]')
# => ArgumentError: Required parameter name is missing.
result = MyInteractor.call(email: '[email protected]', name: 'Bob')
result.success? # => false
puts result.errors # => ['email domain should be yotepresto.com']
-
Context Object:
Context is convenient when you have multiple Interactors called one after the other using the organize method.
So, to make this more predictable, It is recommended only attaching values to the context at the end of the call method or within the
if something.save
... block, as you can see below:
class CreateResponse < BaseInteractor
requires :responder, :answers, :survey
def call
survey_response = responder.survey_responses.build(
response_text: answers[:text],
rating: answers[:rating]
survey: survey
)
if survey_response.save
context.survey_response = survey_response
else
context.fail!(errors: survey_response.errors)
end
end
end
-
Single Responsibility Principle (SRP)
Only one responsability per Class
-
Nesting modules to name space common services together
-
Dependency injection to better isolate service logic in unit tests
-
Contain logic that would otherwise end up in a controller or model
-
Have one concern and generally represent a single chunk of business logic
-
Are available throughout the project code and are not restricted to workflows within a single controller or model
-
Are an alternative to potentially complex callbacks