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:
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.
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 classImportBankFilesincludeInteractordefcall# 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.eachdo|bank_file|boleto=Boleto.find_by(identifier: bank_file.identifier)ifboleto.nil?Sentry.capture_message("not found: #{bank_file.identifier}")nextelseifbank_file.code=='09'# SETTLE BOLETO unlessboleto.settled?boleto.update(paid_at: ...)boleto.order.update(status: ...)PaymentReceivedMailer.send(boleto.payer)endelsifbank_file_code......endendendendend# 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:
# app/interactors/fetch_bank_files_from_ftp.rb classFetchBankFilesFromFTPincludeInteractordefcall# abre conexão com FTP ftp_client=...ftp_connection=ftp_client.connect(...)# pega arquivos e coloca no contexto context.bank_files=ftp_connection.ls...endend# app/interactors/parse_bank_files.rb classParseBankFilesincludeInteractordefcallcontext.parsed_bank_files=context.bank_files.mapdo|bf|# transforma os arquivos txt em objetos ruby...endendend# app/interactors/import_parsed_bank_files.rb classImportParsedBankFilesincludeInteractordefcallcontext.parsed_bank_files.eachdo|parsed_bank_file|...# quita os boletos, atualiza os pedidos e etc. endendend
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 classImportBankFilesincludeInteractor::OrganizerorganizeFetchBankFilesFromFTP,ParseBankFiles,ImportParsedBankFilesendend# 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.
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.
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.
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.
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.
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.
Compose your business logic into commands that sanitize and validate input.
Mutations
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.classUserSignup < Mutations::Command# These inputs are requiredrequireddostring:email,matches: EMAIL_REGEXstring:nameend# These inputs are optionaloptionaldoboolean:newsletter_subscribeend# The execute method is called only if the inputs validate. It does your business action.defexecuteuser=User.create!(inputs)NewsletterSubscriptions.create(email: email,user_id: user.id)ifnewsletter_subscribeUserMailer.async(:deliver_welcome,user.id)userendend# In a controller action (for instance), you can run it:defcreateoutcome=UserSignup.run
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.
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
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
# app/states/charge_state.rbclassChargeState < ApplicationState# @!attribute [r]# Order hash, readonly, requiredargument:order# @!attribute [r]# User model instance readonly, requiredargument:user# @!attribute
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.
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 of 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 in 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 yield an organized and uniform codebase.
Usage
Main concepts and API
As mentioned earlier the operation is an essential concept Pathway is built around. Operations not only structure your code (using steps as will be explained later) but also express meaningful business actions. Operations can be thought…
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.
Ótimo conteúdo!
Teria outros posts falando de outros padrões?
Obrigado por compartilhar!