Utilizando o padrão interactor no Ruby on Rails
Tags: ruby, rails, designpatterns, interactor
Published: 2022-03-20 23:36:54

## 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:

```ruby
# 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
```

A outra opção é tirar isso do controller e jogar a lógica para o modelo:
```ruby
# 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
```

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](https://github.com/collectiveidea/interactor), mas mais adiante no texto apresento outras opções, inclusive uma que eu estou utilizado nos projetos e estou gostando bastante.

```ruby
# 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
```

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:
```ruby
# 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 
```

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:

```ruby
# 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
```

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

```ruby
# 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 
```

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.
{% embed https://github.com/collectiveidea/interactor %}

### 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.
{% embed https://github.com/adomokos/light-service %}

### 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.
{% embed https://github.com/sunny/actor %}


### 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.
{% embed https://github.com/cypriss/mutations %}

### 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.
{% embed https://github.com/hanami/hanami %}

### 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:
{% embed https://github.com/Freshly/flow %}
{% embed https://github.com/serradura/u-case %}
{% embed https://github.com/apneadiving/waterfall %}
{% embed https://github.com/pabloh/pathway %}

## 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.