Como Tratar Casos de Uso de Maneira Prática com Dry-rb

Como Tratar Casos de Uso de Maneira Prática com Dry-rb

Fala pessoal! 👋

Hoje irei tentar mostrar um pouco de como você pode 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

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

Quando iniciamos um novo projeto geralmente ficamos apreensivos e vigilantes para evitar ao máximo cair nas mesmas ciladas pelas quais já passamos.

É normal, contundo, que a medida em que trabalhamos num projeto, o mesmo venha a crescer, e se for o caso, o número de desenvolvedores também aumenta, logo, é necessário adotar uma arquitetura que forneça mecanismos para evolução saudável da sua base de código.

Muitas empresas hoje já entendem a necessidade de se manter um código limpo e de fácil manutenção. Pensando nisso, vamos falar um pouco de duas excelentes gems para ajudar nesses desafio: a dry-monads e a dry-transaction.

Rails new

Quem já está acostumado a usar Ruby on Rails sabe que, inicialmente, o framework sugere uma estrutura de diretório de forma a te guiar pelo clássico e objetivo MVC. Naturalmente, à medida em que o projeto cresce e a complexidade aumenta, esta abordagem tende a ficar cada vez mais complicada de manter.

Talvez o caminho mais conhecido hoje em dia, quando pensamos em manter o design de nossas aplicações, seja iniciar pelos famosos service object.

O objetivo desse artigo é te mostrar uma alternativa prática de evoluir seu projeto sem criar indireções.

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 gems, porém você tem a liberdade de usar apenas uma, duas ou três, então, pra quê sobre-carga?

Dry Transaction ❤️

A dry transaction é focada em te ajudar a definir uma transação de negócio através de vários passos.

O caso de uso

Vamos pensar no processo de registro/cadastro de um usuário, em que o nosso 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.

O fluxo é muito simples e consiste em 3 passos:

fluxo-do-caso-de-use

require 'bundler/inline'

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

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


require 'dry/transaction'
# ...
  1. Valida o input
  2. Insere o registro no banco de dados
  3. Envia o email de boas vindas

Agora vejamos como a representação do caso de uso citado acima fica mais intuitiva usando dry-transaction.

Vou 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)
   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

# 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: 'ari@example.org')

# $ ruby "tmp/example.rb"

# ✉️ Sending welcome email to ari@example.org...
# 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, responderá ao método .call que irá receber os argumentos e passa-los como parametros para o primeiro passo.

Todo o fluxo irá acontecer na ordem em que especificamos acima:

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

Agora, apenas de olhar para esse trecho de código você sabe exatamente o que irá 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: 'ari@example.org')

# $ ruby "tmp/example.rb"

# ✉️ Sending welcome email to ari@example.org...
# Success("Please, confirm your email. 😉")

Teremos o mesmo resultado! Incrivelmente simples!

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! 👋

Links e referências

Comentários