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
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
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
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
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
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
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.
collectiveidea / interactor
Interactor provides a common interface for performing complex user interactions.
Interactor
Getting Started
Add Interactor to your Gemfile and bundle install
.
gem "interactor", "~> 3.0"
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
Failing the Context
When something goes wrong in your interactor, you can flag the context as failed.
context.fail!
When given a hash argument, the fail!
method can also update the context. The
following are equivalent:
context.error = "Boom!"
context.fail!
context.fail!(error:
…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.
adomokos / light-service
Series of Actions with an emphasis on simplicity.
LightService is a powerful and flexible service skeleton framework with an emphasis on simplicity
Table of Contents
- Table of Contents
- Why LightService?
- Getting started
- Stopping the Series of Actions
- Benchmarking Actions with Around Advice
- Before and After Action Hooks
- Expects and Promises
- Key Aliases
- Logging
- Error Codes
- Action Rollback
- Localizing Messages
- Orchestrating Logic in Organizers
- ContextFactory for Faster Action Testing
- Rails support
- Other implementations
- Contributing
- Release Notes
- License
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 =>
…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.
ServiceActor
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.
Contents
Installation
Add the gem to your application’s Gemfile by executing:
bundle add service_actor
Extensions
For Rails generators, you can use the service_actor-rails gem:
bundle add service_actor-rails
For TTY prompts, you can use the service_actor-promptable gem:
bundle add service_actor-promptable
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
Trigger them in your application with .call
:
SendNotification
…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.
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.
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
…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.
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:
- Hanami::Router - Rack compatible HTTP router for Ruby
- Hanami::Controller - Full featured, fast and testable actions for Rack
- Hanami::View - Presentation with a separation between views and templates
- Hanami::Helpers - View helpers for Ruby applications
- Hanami::Mailer - Mail for Ruby applications
- Hanami::Assets - Assets management for Ruby
These components are designed to be used independently or together in a Hanami application.
Status
Installation
Hanami supports Ruby (MRI) 3.0+
gem install hanami
Usage
hanami new bookshelf
cd bookshelf && bundle
bundle exec hanami server # visit http://localhost:2300
Please follow along with the Getting Started guide.
Donations
You can give back to Open Source…
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:
Flow
Installation
Add this line to your application's Gemfile:
gem "flow"
Then, in your project directory:
$ bundle install
$ rails generate flow:install
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
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.rb
class ChargeState < ApplicationState
# @!attribute [r]
# Order hash, readonly, required
argument :order
# @!attribute [r]
# User model instance readonly, required
argument :user
# @!attribute
…serradura / 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.
The main project goals are:
- Easy to use and easy to learn (input >> process >> output).
- Promote immutability (transforming data instead of modifying it) and data integrity.
- No callbacks (ex: before, after, around) to avoid code indirections that could compromise the state and understanding of application flows.
- Solve complex business logic, by allowing the composition of use cases (flow creation).
- 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
apneadiving / waterfall
A slice of functional programming to chain ruby services and blocks, thus providing a new approach to flow control. Make them flow!
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
bychain
- red its bypassed and only
on_dam
blocks are executed.
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
…Pathway
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…
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.
Top comments (2)
Obrigado por compartilhar o artigo. Deixou de forma simples e fácil de digerir.
Ótimo conteúdo!
Teria outros posts falando de outros padrões?
Obrigado por compartilhar!