DEV Community

Stephann
Stephann

Posted on • Updated on

Utilizando o padrão interactor no Ruby on Rails

Introdução

Um problema que é comum e que todo desenvolvedor Rails já se deparou, é ver a qualidade da base de código se deteriorar conforme as regras de negócio vão aumentando e ficando mais complexas. Numa equipe ainda sem muita experiência, é quase certo que vão aparecer na aplicação os famosos fat controllers. Os controllers gordos são comuns e eles nada mais são que aqueles controllers cheios de responsabilidades e comportamentos que deveriam estar em outras camadas. Depois que o desenvolvedor aprende na prática e entende que fat controllers são prejudiciais, ele tende a começar a escrever quase toda lógica de negócio nos modelos, e que o leva aos fat models. O problema dos modelos gordos é o mesmo, é uma classe com vários comportamentos centralizados e que deveriam estar distribuídos de uma melhor forma. Com a ideia de ajudar a fugir dos modelos e controllers mais gordinhos, os concerns foram introduzidos no Rails. Eles são módulos que permitem definir comportamentos em um arquivo separado e que podem ser incluídos em outras classes. Mas enquanto os concerns ajudam a escrever modelos e controllers menores em quantidade de linhas, não resolvem o problema deles estarem estarem inchados de comportamentos.

Aí o desenvolvedor se questiona: "Então se fat controllers são ruins e fat models também, onde vou colocar meu código? Na camada de view? Ou devo abandonar a profissão de desenvolvedor e vender minha arte na praia?". Nenhuma das duas opções. Há alguns padrões de design que ajudam a organizar seu código sem entupir uma classe de métodos e responsabilidades, tipo os Form Objects, Service Objects, Query Objects, Clients, Interactors e etc. Nesse artigo vou apresentar alguns conceitos dos interactors, mostrar algumas gems que já testei e exemplificar o uso do padrão com situações quase reais.

Conceito

Primeiro quero deixar claro que não há um consenso exato na comunidade sobre o que é um interactor. Você vai encontrar uma galera na internet usando os termos service objects, operations, use-cases, mutations, commands ao se referir a um mesmo conceito, já outro desenvolvedor saberá dizer a diferença entre cada um, a outra irá discordar e explicará de outra maneira, mas de forma geral, estarão falando sobre as mesmas coisas ou pelo menos sobre soluções bem parecidas. O fato é que um interactor nada mais é do que um objeto simples, com um propósito único, que encapsula sua regra de negócio e representa uma funcionalidade da sua aplicação. Explicando com um exemplo: Sua aplicação é uma newsletter e você deve enviar um e-mail para todos os assinantes toda vez que um artigo novo for criado e ele não estiver marcado como rascunho. Uma das soluções é fazer isso no controller:

# app/controllers/articles_controllers.rb 
class ArticlesController < ApplicationController 
  def create 
    @article = Article.new(post_params) 

    if @article.save 
      unless @article.draft? 
        Member.where(subscribed: true).each do |member| 
          SendMailJob.perform_async(member.id, @article.id) 
        end 
      end 

      redirect_to @article, notice: 'Success!' 
    else 
      render :new 
    end 
  end 
end
Enter fullscreen mode Exit fullscreen mode

A outra opção é tirar isso do controller e jogar a lógica para o modelo:

# app/models/article.rb 
class Article < ApplicationRecord 
  after_create :send_mail_to_members, unless: :draft? 

  def send_mail_to_members 
    Member.where(subscribed: true).each do |member| 
      SendMailJob.perform_async(member.id, self.id) 
    end 
  end 
end
Enter fullscreen mode Exit fullscreen mode

Se tua intenção é evitar código acoplado e a duplicação de código, as duas soluções acima não te ajudam nesses pontos. Além de atribuir responsabilidades que fogem do escopo dessas classes, colocar o comportamento no controller torna mais difícil a reutilização em outros locais da aplicação e colocar num callback do ActiveRecord, você enterra esse comportamento num funcionamento interno da classe, dificultando a manutenção, a implementação de testes e causando surpresas para quem for usar aquela classe um dia e descobrir que acabou enviando alguns milhares de e-mails só porque salvou um artigo na tabela.

Para ajudar resolver esses problemas, o interactor entra em cena e essa situação seria resolvida da seguinte forma:

Uma observação antes de mostrar o código: Vou usar nos exemplos a gem interactor, mas mais adiante no texto apresento outras opções, inclusive uma que eu estou utilizado nos projetos e estou gostando bastante.

# app/interactors/create_article.rb 
class CreateArticle 
  include Interactor 

  def call 
    context.article = Article.new(context.params) 

    if context.article.save
      unless context.article.draft? 
        Member.where(subscribed: true).each do |member| 
          SendMailJob.perform_async(member.id, article.id) 
        end 
      end 
    else 
      context.fail! unless context.article.valid? 
    end 
  end 
end 

# app/controllers/articles_controller.rb 
class ArticlesController < ApplicationController 
  def create 
    result = CreateArticle.call(params: article_params) 

    if result.success? 
      redirect_to result.article, notice: 'Success!' 
    else
      @article = result.article 
      render :new 
    end 
  end 
end
Enter fullscreen mode Exit fullscreen mode

Dessa forma o controller não precisa mais saber sobre a regra de negócio, o papel dele é receber a requisição, delegar o processamento dos dados e depois cuidar de devolver uma resposta. E você também não sujou o modelo com callbacks.

Vale a pena notar que diferente do que aprendemos nas cadeiras Orientação a Objeto, a classe CreateArticle não representa a abstração de um objeto com propriedades, métodos, com herança, polimorfismo e etc, mas sim uma ação, como o próprio nome já representa: CriarArtigo. Quase como que voltando para a programação estrutural mas em uma escala de caso de uso. O mesmo esquema poderia ser utilizado para outras coisas como FinalizarPedido, ProcessarWebhook, ImportarArquivos e etc. Onde toda a lógica de verificar estoque pra confirmar pedido, verificar a assinatura do webhook, ler, transformar e importar dados de arquivos ficariam em interactors, e não mais em modelos ou controllers.

Organizando os interactors

Não é porque agora você colocou interactors na aplicação que tudo vai ficar mil maravilhas e nada de ruim vai acontecer. Também corre o risco de ter interactors gigantes, com códigos repetidos, fazendo mais coisas do que deveriam. Por isso há um conceito de orquestradores (também chamados de organizadores) no padrão interactor, que são basicamente interactors que executam outros interactors. Vou trazer um novo exemplo pra ficar claro os benefícios desses orquestradores: Minha aplicação precisa importar e processar os arquivos de retorno bancário da FEBRABAN para saber se os boletos que minha aplicação emitiu foram pagos ou não. O passo a passo desse processo é o seguinte: Pegar os arquivos no FTP, transformar os dados arquivos em objetos ruby, e processar esses arquivos atualizando os boletos e pedidos do sistema. Utilizando apenas um interactor seria mais ou menos assim:

# app/interactors/import_bank_files.rb 
class ImportBankFiles 
  include Interactor 

  def call 
    # abre conexão com FTP 
    ftp_client = ... 
    ftp_connection = ftp_client.connect(...) 

    # pega arquivos 
    bank_files = ftp_connection.ls...

    # lê arquivos txt e transforma em objetos ruby 
    parsed_bank_files = bank_files.map { ... } 

    # processa arquivos 
    parsed_bank_files.each do |bank_file| 
      boleto = Boleto.find_by(identifier: bank_file.identifier) 

      if boleto.nil? 
        Sentry.capture_message("not found: #{bank_file.identifier}") 
        next 
      else
        if bank_file.code == '09' # SETTLE BOLETO 
          unless boleto.settled? 
            boleto.update(paid_at: ...) 
            boleto.order.update(status: ...) 
            PaymentReceivedMailer.send(boleto.payer) 
          end 
        elsif bank_file_code ... 
          ... 
        end 
      end 
    end 
  end 
end 

# Em algum outro lugar para executar a importação 
ImportBankFiles.call 
Enter fullscreen mode Exit fullscreen mode

Abstraí bem os detalhes da implementação porque não é a intenção mostrar como processar arquivos de retorno bancário, e sim exemplificar um processo mais complexo. Esse código poderia estar pior e estaria se estivesse, por exemplo, num controller ou num modelo, mas temos como melhorar dividindo as responsabilidades em interactors menores e chamando tudo junto com um orquestrador. Primeiro destrinchando o comportamento em múltiplos interactors:

# app/interactors/fetch_bank_files_from_ftp.rb 
class FetchBankFilesFromFTP 
  include Interactor 

  def call 
    # abre conexão com FTP 
    ftp_client = ... 
    ftp_connection = ftp_client.connect(...) 

    # pega arquivos e coloca no contexto 
    context.bank_files = ftp_connection.ls... 
  end 
end 

# app/interactors/parse_bank_files.rb 
class ParseBankFiles 
  include Interactor 

  def call 
    context.parsed_bank_files = context.bank_files.map do |bf| 
      # transforma os arquivos txt em objetos ruby
      ... 
    end 
  end 
end 

# app/interactors/import_parsed_bank_files.rb 
class ImportParsedBankFiles 
  include Interactor 

  def call 
    context.parsed_bank_files.each do |parsed_bank_file| 
      ... # quita os boletos, atualiza os pedidos e etc. 
    end 
  end 
end
Enter fullscreen mode Exit fullscreen mode

Atenção nos dados sendo compartilhados via contexto para outros interactors poderem acessá-los. Agora junta tudo com um orquestrador:

# app/organizers/import_bank_files.rb 
class ImportBankFiles 
  include Interactor::Organizer 

  organize FetchBankFilesFromFTP, 
    ParseBankFiles, 
    ImportParsedBankFiles 
  end 
end

# Usando o orquestrador em algum lugar da aplicação 
ImportBankfiles.call 
Enter fullscreen mode Exit fullscreen mode

Dividir os interactors dessa forma traz algumas vantagens: Deixa o código mais manutenível, facilita a implementação dos testes e ainda torna o código reutilizável. Vamos supor que agora a aplicação precisa permitir o recebimento de arquivos bancários por um formulário para caso o FTP esteja com problema. Você criaria mais um orquestrador chamado de UploadBankFiles, e você conseguiria reutilizar pelo menos o ParseBankFiles e o ImportParsedBankFiles no fluxo de recebimento desses arquivos.

Gems disponíveis

Implementar o padrão interactor não é tão complicado. É basicamente um PORO com um método público chamado #call (ou #run, ou #execute ou qualquer coisa que você preferir). Depois você pode ir incrementando sua própria implementação do padrão, adicionando mensagens, tratamento de erros, contextos, rollbacks, orquestradores e assim por diante. Mas o que não falta são gems pra você adicionar no projeto e já começar a escrever interactors agora. Vou colocar a seguir uma lista com as gems que eu pelo menos li a documentação, e as que eu já usei em algum projeto eu vou deixar alguns breves comentários.

collectiveidea/interactor:

Essa foi a primeira que usei. Gosto dela pela simplicidade e tem uma boa API permitindo mais flexibidade mas não fornece uma opção de declarar os parâmetros de entrada e saída de um interactor.

GitHub logo collectiveidea / interactor

Interactor provides a common interface for performing complex user interactions.

Interactor

Gem Version Build Status Maintainability Test Coverage Ruby Style Guide

Getting Started

Add Interactor to your Gemfile and bundle install.

gem "interactor", "~> 3.0"
Enter fullscreen mode Exit fullscreen mode

What is an Interactor?

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.

Context

An interactor is given a context. The context contains everything the interactor needs to do its work.

When an interactor does its single purpose, it affects its given context.

Adding to the Context

As an interactor runs it can add information to the context.

context.user = user
Enter fullscreen mode Exit fullscreen mode

Failing the Context

When something goes wrong in your interactor, you can flag the context as failed.

context.fail!
Enter fullscreen mode Exit fullscreen mode

When given a hash argument, the fail! method can also update the context. The following are equivalent:

context.error = "Boom!"
context.fail!
Enter fullscreen mode Exit fullscreen mode
context.fail!(error: 
Enter fullscreen mode Exit fullscreen mode

adomokos/light-service:

O diferencial da light-service é que ela tem uma API mais avançada para os orquestradores, mais opções para o controle de fluxo e também permite uma declarar os parâmetros de entrada e os resultados gerados pelo interactor.

GitHub logo adomokos / light-service

Series of Actions with an emphasis on simplicity.

LightService

Gem Version CI Tests codecov Code Climate License Download Count

LightService is a powerful and flexible service skeleton framework with an emphasis on simplicity

Table of Contents

Why LightService?

What do you think of this code?

class TaxController < ApplicationController
  def update
    @order = Order.find(params[:id])
    tax_ranges = TaxRange.for_region(order.region)
    if tax_ranges.nil?
      render :action => :edit, :error => "The tax ranges were not found"
      return # Avoiding the double render error
    end

    tax_percentage =
Enter fullscreen mode Exit fullscreen mode

sunny/actor:

Essa é a minha preferida. Estou usando ela em todos os meus projetos. Ela não tem um orquestrador tão versátil quanto os da light-service, mas o que tem lá me serve bem, e a definição de parâmetros ajuda bastante pois tem como definir algumas opções, como tipos e valores padrões.

GitHub logo sunny / actor

Composable Ruby service objects

Actor

Tests

This Ruby gem lets you move your application logic into into small composable service objects. It is a lightweight framework that helps you keep your models and controllers thin.

Photo of theater seats

Contents

Installation

Add these lines to your application’s Gemfile:

# Composable service objects
gem 'service_actor'
Enter fullscreen mode Exit fullscreen mode

When using Rails, you can include the service_actor-rails gem:

# Composable service objects
gem "service_actor-rails"
Enter fullscreen mode Exit fullscreen mode

Usage

Actors are single-purpose actions in your application that represent your business logic. They start with a verb, inherit from Actor and implement a call method.

# app/actors/send_notification.rb
class SendNotification < Actor
  def call
    # …
  end
end
Enter fullscreen mode Exit fullscreen mode

Trigger them in your application with .call:

SendNotification.call # => <ServiceActor::Result…>
Enter fullscreen mode Exit fullscreen mode

When called, actors return a Result. Reading and writing…

cypriss/mutations:

Usei essa em alguns projetos, mas a falta de orquestradores, a dificuldade de encadear chamadas e a definição de parâmetros de entrada e a burocracia de uso deles me afastaram.

GitHub logo cypriss / mutations

Compose your business logic into commands that sanitize and validate input.

Mutations

Build Status Code Climate

Compose your business logic into commands that sanitize and validate input. Write safe, reusable, and maintainable code for Ruby and Rails apps.

Installation

gem install mutations

Or add it to your Gemfile:

gem 'mutations'

Example

# Define a command that signs up a user.
class UserSignup < Mutations::Command
  # These inputs are required
  required do
    string :email, matches: EMAIL_REGEX
    string :name
  end

  # These inputs are optional
  optional do
    boolean :newsletter_subscribe
  end

  # The execute method is called only if the inputs validate. It does your business action.
  def execute
    user = User.create!(inputs)
    NewsletterSubscriptions.create(email: email, user_id: user.id) if newsletter_subscribe
    UserMailer.async(:deliver_welcome, user.id)
    user
  end
end

# In a controller action (for instance), you can run it:
def create
  outcome = UserSignup.run
Enter fullscreen mode Exit fullscreen mode

hanami/hanami:

Hanami não é uma gem de interactor e sim um framework web completo, como o Rails é. Mas ele traz internamente já uma solução do padrão para uso opcional por quem preferir seguir essa estratégia. Kudos para o pessoal do Hanami.

GitHub logo hanami / hanami

The web, with simplicity.

Hanami 🌸

The web, with simplicity.

Version

This branch contains the code for hanami 2.0.x.

Frameworks

Hanami is a full-stack Ruby web framework It's made up of smaller, single-purpose libraries.

This repository is for the full-stack framework which provides the glue that ties all the parts together:

These components are designed to be used independently or together in a Hanami application.

Status

Gem Version CI Test Coverage Depfu Inline Docs

Installation

Hanami supports Ruby (MRI) 3.0+

gem install hanami
Enter fullscreen mode Exit fullscreen mode

Usage

hanami new bookshelf
cd bookshelf && bundle
Enter fullscreen mode Exit fullscreen mode

Outras

A seguir coloquei as que eu já li as documentações, considerei uso, mas por um ou outro motivo não decidi testar em algum projeto:

GitHub logo Freshly / flow

Write modular and reusable business logic that's understandable and maintainable.

Flow

Gem Version Build Status Maintainability Test Coverage

Installation

Add this line to your application's Gemfile:

gem "flow"
Enter fullscreen mode Exit fullscreen mode

Then, in your project directory:

$ bundle install
$ rails generate flow:install
Enter fullscreen mode Exit fullscreen mode

What is Flow?

Flow is a SOLID implementation of the Command Pattern for Ruby on Rails.

Flows allow you to encapsulate your application's business logic into a set of extensible and reusable objects.

Quickstart Example

Install Flow to your Rails project:

$ rails generate flow:install
Enter fullscreen mode Exit fullscreen mode

Then define State, Operation(s), and Flow objects.

State

A State object defines data that is to be read or written in Operation objects throughout the Flow. There are several types of data that can be defined, such as argument, option, and output.

$ rails generate flow:state Charge
Enter fullscreen mode Exit fullscreen mode
# app/states/charge_state.rb
class ChargeState < ApplicationState
  # @!attribute [r]
  # Order hash, readonly, required
  argument :order
  # @!attribute [r]
  # User model instance readonly, required
  argument :user

  # @!attribute
Enter fullscreen mode Exit fullscreen mode

GitHub logo serradura / u-case

Represent use cases in a simple and powerful way while writing modular, expressive and sequentially logical code.

u-case - Represent use cases in a simple and powerful way while writing modular, expressive and sequentially logical code.

Represent use cases in a simple and powerful way while writing modular, expressive and sequentially logical code.


Ruby Gem Build Status Maintainability Test Coverage

The main project goals are:

  1. Easy to use and easy to learn (input >> process >> output).
  2. Promote immutability (transforming data instead of modifying it) and data integrity.
  3. No callbacks (ex: before, after, around) to avoid code indirections that could compromise the state and understanding of application flows.
  4. Solve complex business logic, by allowing the composition of use cases (flow creation).
  5. Be fast and optimized (Check out the benchmarks section).

Note: Check out the repo https://github.com/serradura/from-fat-controllers-to-use-cases to see a Rails application that uses this gem to handle its business logic.

Documentation

Note: Você entende português? 🇧🇷 🇵🇹 Verifique o README traduzido em pt-BR.

Table of Contents


GitHub logo apneadiving / waterfall

A slice of functional programming to chain ruby services and blocks, thus providing a new approach to flow control. Make them flow!

Code Climate Test Coverage Build Status Gem Version

Goal

Chain ruby commands, and treat them like a flow, which provides a new approach to application control flow.

When logic is complicated, waterfalls show their true power and let you write intention revealing code. Above all they excel at chaining services.

Material

Upcoming book about failure management patterns, leveraging the gem: The Unhappy path

General presentation blog post there: Chain services objects like a boss.

Reach me @apneadiving

Overview

A waterfall object has its own flow of commands, you can chain your commands and if something wrong happens, you dam the flow which bypasses the rest of the commands.

Here is a basic representation:

  • green, the flow goes on, chain by chain
  • red its bypassed and only on_dam blocks are executed.

Waterfall Principle

Example

class FetchUser
  include Waterfall
  def initialize(user_id)
    @user_id = user_id
  end
  def call
    chain { @response = HTTParty.get("https://jsonplaceholder.typicode.com/users/#{@user_id
Enter fullscreen mode Exit fullscreen mode

GitHub logo pabloh / pathway

Define your business logic in simple steps

Pathway

Gem Version CircleCI Coverage Status

Pathway encapsulates your business logic into simple operation objects (AKA application services on the DDD lingo).

Installation

$ gem install pathway

Description

Pathway helps you separate your business logic from the rest of your application; regardless if is an HTTP backend, a background processing daemon, etc The main concept Pathway relies upon to build domain logic modules is the operation, this important concept will be explained in detail the following sections.

Pathway also aims to be easy to use, stay lightweight and extensible (by the use of plugins), avoid unnecessary dependencies, keep the core classes clean from monkey patching and help yielding an organized and uniform codebase.

Usage

Main concepts and API

As mentioned earlier the operation is a crucial concept Pathway leverages upon. Operations not only structure your code (using steps as will be explained later) but also express meaningful business actions. Operations can be thought as use…

Conclusão

Esse foi um resumo bem geral sobre o padrão interactor, com mais exemplos práticos do que teorias propriamente ditas, mas que de toda forma ajuda a apresentar os conceitos, o uso e incentiva os desenvolvedores começarem a testar nos seus projetos pessoais ou naqueles testes técnicos para seleção de candidatos. Mas vale lembrar sempre: como tudo no desenvolvimento de software, não há bala de prata, esse padrão apresentado não vai resolver todos os problemas e nem será a melhor opção para todos os casos que aparecerem, mas vale a pena conhecer mais para enriquecer o portifólio de estratégias e soluções poderão ser útil ao desenvolvedor em algum momento da carreira.

Discussion (1)

Collapse
joathan profile image
Joathan Francisco

Ótimo conteúdo!
Teria outros posts falando de outros padrões?
Obrigado por compartilhar!