Skip to content
This repository has been archived by the owner on Dec 29, 2024. It is now read-only.

Proposal For New Interactor Pattern #84

Closed
aaronmallen opened this issue Oct 31, 2019 · 0 comments
Closed

Proposal For New Interactor Pattern #84

aaronmallen opened this issue Oct 31, 2019 · 0 comments
Labels
discussion A discussion help wanted Extra attention is needed

Comments

@aaronmallen
Copy link
Owner

aaronmallen commented Oct 31, 2019

Problem

During the discussion in #74 it occurred to me that there maybe an unnecessary fragmentation or friction with the way interactors and their contexts are currently defined.

Proposal

I propose a whole new pattern for interactor definition that would be some what similar to how rake tasks are written in a given project. The idea would be you would define an interactor namespace like :user and within that namespace you would define a set of contexts and a set of interactors like this:

# interactors/user.rb

ActiveInteractor::InteractorNamespace.new(:user) do
  
  # we define a default interactor context
  # this context would be used by all interactors in this namespace
  # unless the interactor specifically specifies otherwise  
  context do
    attributes :first_name, :last_name, :email, :user
  end
 
  context :user_and_account do
    attributes :account, :user
    validates :account, on: :called
  end

  context :create_account do
    attributes :account, :user
    validates :user, presence: true, on: :calling 
  end
  
  # defining an interactor would be the equivalent of defining the #perform
  # method in current interactor classes. 
  # this interactor would use the default context as it doesn't
  # specify one nor is there a context with a matching name
  interactor :finder do |context|
    context.user = User.find_by(
      email: context.email,
      first_name: context.first_name, 
      last_name: context.last_name
     )
  end
  
  # this interactor would use the default context as it doesn't
  # specify one nor is there a context with a matching name
  interactor :downcase_email do |context|
    context.email = context.email&.downcase
  end
  
  # this interactor has a dependency on another interactor
  interactor creator: %i[downcase_email] do |context|
    context.user ||= User.create(
      email: context.email,
      first_name: context.first_name, 
      last_name: context.last_name
    )
  end

  # we don't need to specify a context here it will
  # use the :create_account context by default
  interactor :create_account do |context|
    context.account = context.user.generate_account!
  end

  # we can define an organizer that specifies a context
  organizer :create_with_account, contextualize_with: :user_and_account do
    organize :finder, :creator, :create_account
  end
end

given this pattern we can then just call a single simple api to invoke the interaction like this:

context = ActiveInteractor.perform(
  :'user:create_with_account', 
  email: 'hello@aaronmallen.me', 
  first_name: 'Aaron', 
  last_name: 'Allen'
)

context.success? #=> true
context #=> <# Interactor::User::UserAndAccountContext ...>

additionally we can call a different interactor within that namespace

context = ActiveInteractor.perform(
  :'user:finder', 
  email: 'hello@aaronmallen.me', 
  first_name: 'Aaron', 
  last_name: 'Allen'
)

context.success? #=> true
context #=> <# Interactor::User::DefaultContext ...>

Additionally we could even provide helper methods for rails ActionController::Base so that this:

# user_controller.rb
def create
  result = ActiveInteractor.perform(:'user:create_with_account', user_params)
  if result.success?
    render json: result.user, status: :created
  else
    render json: { errors: result.user.errors }, status: :unprocessable_entity
  end
end

could be refactored to this:

# user_controller.rb
def create
  render_interactor_result_json(:'user:create_with_account', user_params)
end

The goal here would be to provide a single api for every interactor in your project something like:

ActiveInteractor.perform(interactor_name, context_attributes = {})

Things to consider before answering

  1. Do you think this pattern would be overall a better approach for the interactor pattern?
  2. Would you find value in being able to call one method to run all of your interactors?
@aaronmallen aaronmallen added the discussion A discussion label Oct 31, 2019
@aaronmallen aaronmallen pinned this issue Oct 31, 2019
@aaronmallen aaronmallen added the help wanted Extra attention is needed label Oct 31, 2019
Repository owner locked as resolved and limited conversation to collaborators Dec 10, 2019
@aaronmallen aaronmallen unpinned this issue Dec 10, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
discussion A discussion help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

1 participant