Hoje irei tentar mostrar um pouco de como tratar os casos de uso dentro de suas aplicações Ruby On Rails qualquer aplicação Ruby usando uma “coleção de gems” rotuladas como a próxima geração de bibliotecas Ruby - dry-rb💡.

“dry-rb helps you write clear, flexible, and more maintainable Ruby code. Each dry-rb gem fulfils a common task, and together they make a powerful platform for any kind of Ruby application.”

Ao embarcar em um novo projeto, é natural sentir uma mistura de excitação e cautela, especialmente ao esforçar-se para não repetir erros do passado.Frequentemente, projetos tendem a se expandir à medida que avançam, o que, por sua vez, pode aumentar o número de desenvolvedores envolvidos. Nesse contexto, é essencial pensar/implementar uma arquitetura robusta, equipada com recursos que assegurem um desenvolvimento contínuo e saudável da code base. Essa abordagem é crucial para promover a escalabilidade e facilitar a manutenção do projeto no longo prazo. Hoje em dia, qualquer empresa que se preocupe em “avancar bem” deve considerar investimentos neste sentido. Pensando nisso, irei mostrar gemas que ajudam a promover estes beneficios em projetos Ruby e Ruby on Rails - dry-monads e a dry-transaction.

Rails new

Para aqueles que já estão familiarizados com o uso de Ruby on Rails é de conhecimento comum que o framework adota, de inicio, uma estrutura de diretórios baseada no MVC padrão(Model-View-Controller), conhecido por sua clareza e eficácia. Contudo, à medida que o projeto cresce e sua complexidade se intensifica, manter essa estrutura pode se tornar progressivamente desafiador. Talvez o caminho mais conhecido, quando pensamos em manter o design, seja o service object mas ele também não está isento de desafios, especialmente relacionado ao acoplamento e indirecao.

Começando com Dry-rb

O dry-rb, como expliquei anteriormente, é um grupo de gems que juntas te ajudam a resolver vários problemas simples do dia-a-dia, ou seja, você não precisamos perder tempo tentando reinventar a roda. 😜 Atualmente, o dry-rb é composto por mais de 20 gemas. No entanto, é importante destacar que você tem a liberdade de utilizar apenas as que são relevantes de acordo com seus objetivos e necessidades.

Dry Transaction

A dry-transaction é focada em te ajudar a definir uma transação de negócio através de vários passos. Vamos pensar no processo de registro/cadastro de um usuário em um sistema. O fluxo é muito simples e consiste em 3 passos:

O caso de uso

Ok, vamos pensar no processo de registro/cadastro de um usuário em um sistema.

💡 Em todos os exemplos vou estar usando o bundle inline pra ter realmente um exemplo que funcione. Em alguns momentos eu irei ocultar esse trecho para evitar que os exemplos fiquem muito grandes.

require 'bundler/inline'

gemfile do
 source 'https://rubygems.org'

 gem 'dry-monads'
 gem 'dry-transaction'
end


require 'dry/transaction'
# ...

A ideia é que o usuário preencha um formulário com seu nome e e-mail. O fluxo é muito simples e consiste em 3 passos:

  1. O primeiro passo é validar o input do usuário(Não o formato e sim espeficacoes do negócio).
  2. O segundo passo é inserir o registro no banco de dados.
  3. O terceiro passo é enviar um e-mail de boas vindas.

Agora, vamos observar como a representação do caso de uso mencionado acima pode ser tornada mais intuitiva através do uso do dry-transaction. Para tal, irei te apresentar duas formas: A primeira, mais convencional, é encotrada até como exemplo de uso na própria documentação da dry-transaction

# example.rb
# ... bundle inline acima.👆🏻

class CreateUser
 include Dry::Transaction

 step :validate
 step :create
 step :welcome_email

 # Database representation
 DB = []

 private

 def validate(params)
   # Verify bussiness rules of the input ...
   if params[:name] && params[:email]
     Success(params)
   else
     Failure("ops... Name and email must present!")
   end
 end

 def create(params)
   if DB.push(params)
     Success(params[:email])
   else
     Failure("Panic!!! 🐞")
   end
 end

 def welcome_email(email)
   # UserMailer.welcome(email).deliver_later

   puts "✉️ Sending welcome email to #{email}..."
   Success('Please, confirm your email. 😉')
 end
end

# New instance of CreateUser class
obj = CreateUser.new

# The call method will receive the arguments and pass them as parameters to the first step.
obj.call(name: 'Aristóteles', email: '[email protected]')

# $ ruby "tmp/example.rb"

# ✉️  Sending welcome email to [email protected]...
# Success("Please, confirm your email. 😉")

Perceba que não precisamos declarar explicitamente um método construtor na classe CreateUser. Após iniciarmos um objeto, este, por sua vez, responderá ao método .call que irá receber os argumentos e passa-los como parametros para o primeiro passo.

Todo o fluxo acontece na ordem em que especificamos acima, o que traz clareza e previsibilidade sobre o processo. Ex.

 step :validate      # 1
 step :create        # 2
 step :welcome_email # 3

Agora, apenas olhando para o trecho de código é possível entender quais os passos que irão acontecer no processo de cadastro de uma nova conta.

Uma outra abordagem muito comum, é o usar classes para isolar cada passo do nosso processo, para isso, você pode “empacotar suas operações” com a ajuda do Container, porém, neste segundo exemplo, eu vou mostrar uma forma que não é apresentada na documentação oficial, um pequeno arranjo que aprendi numa talk da Camila Campos, na Ruby Summit em Dezembro de 2020.

A ideia é criar uma classe para cada passo que representa o caso de uso. Com as classes com um único método público “call”, ou seja, ao invés de referenciarmos métodos iremos referenciar classes. Vamos refazer o exemplo anterior e ver como funciona na prática. Uma diferença nos exemplos a seguir é que vamos fazer um include explicito em cada classe separada do dry-monads para tratar os casos de sucesso e falha com uso da mônade Either

# create_user.rb
require 'bundler/inline'

gemfile do
 source 'https://rubygems.org'

 gem 'dry-monads'
 gem 'dry-transaction'
end

require 'dry/monads'
require 'dry/transaction'

# ..
# ..

class CreateUser
 include Dry::Transaction

 def initialize
   steps = {
     validate: NormalizeParams.new,
     create_record: CreateRecord.new,
     welcome_email: WelcomeEmail.new
   }

   super(**steps)
 end

 step :validate
 step :create
 step :welcome_email

 # Database representation
 DB = []
end
# normalize_params.rb

class NormalizeParams
 include Dry::Monads[:result]

 def call(**params)
   return Failure(message: 'ops... Name and email must present!') if params[:name].nil? && params[:email].nil?

   Success(params)
 end
end
# create_record.rb

class CreateRecord
 include Dry::Monads[:result]

 def call(params)
   return Success(params) if CreateUser::DB.push(params)

   Failure(message: "Panic!!! 🐞")
 end
end
# welcome_email.rb

class WelcomeEmail
 include Dry::Monads[:result]

 def call(params)
   # UserMailer.welcome(email).deliver_later

   puts "✉️ Sending welcome email to #{email}..."
   Success('Please, confirm your email. 😉')
 end
end

Testando tudo!

# Instância um novo objeto da class CreateUser
obj = CreateUser.new

# Objeto instanciado agora responde ao método call
obj.call(name: 'Aristóteles', email: '[email protected]')

# $ ruby "tmp/example.rb"

# ✉️ Sending welcome email to [email protected]...
# Success("Please, confirm your email. 😉")

Teremos o mesmo resultado e o resultado será o mesmo, mas a diferença é que agora temos uma classe para cada passo do nosso processo. Isso pode ser útil para testes, por exemplo, ou para reutilizar esses passos em outros casos de uso.

Conclusão

O ecossistema dry-rb é incrível e muito bem mantido por dezenas de pessoas ao redor do mundo. Mesmo que você não o use diretamente em seu projeto/empresa, acredito que vale a pena conferir a documentação, isso até antes mesmo de tentar criar algo do zero. Lembre-se, você não precisa usar todas as gemas, procure usar apenas as que vão atender as suas necessidades Se você chegou neste assunto então deve saber que “não existe bala de prata”. Você usa o que for melhor para a sua empresa/projeto ou o que fizer mais sentido para sua equipe.

Com isso eu encerro meus 10 centavos de contribuição. Até a próxima! 👋

  • Dry-rb 👉🏻 https://dry-rb.org
  • Talk da Camila Campus na Ruby Summit 👉🏻 https://youtu.be/T2eRCJ-cRfA
  • Gist com todo o código 👉🏻 https://gist.github.com/aristotelesbr/917d82e832712a9a4c27387a2b3fc356