Usando a composição ao seu favor

Conheçendo o dry-monads

Nesse post falo um pouco de como o dry-monads facilita/estimula o uso de composição e como tenho usado no dia-a-dia.

ROP-result

Recentemente, aqui JUS, temos adotado, o uso de algumas gems que compõem o ecossistema dry, uma delas é a dry-monads.

"A dry-domanad é um conjunto de mônadas comuns para Ruby. As mônadas fornecem uma maneira elegante de lidar com erros, exceções e funções de encadeamento para que o código seja muito mais compreensível e tenha todo o tratamento de erros, evitando assim ter um código cheio de ifs."

O uso de mônadas de continuidade tem melhorado significativamente não só a organização e qualidade como também tem ajudado a melhorar a nossa forma de pensar.

Ultimamente, tenho estudado bastante sobre o paradigma funcional em si e com uma ajuda do @RodrigoSerradura, eu tenho conseguido evoluir bastante o pensamento e a aplicabilidade voltados para o Ruby.

Irei mostrar como dry-monads vem favorecendo esse pensamento no meu dia-a-dia.

💡 Estou usando bundle inline para tonar tudo que escrever um código executável de fato.

Considere o seguinte:

require 'bundler/inline'

gemfile do
source 'https://rubygems.org'
gem 'dry-monads'
end

require 'dry/monads'

class ResolveFullName
 include Dry::Monads[:result, :do]

 def call(first_name:, last_name:, **)
   first = String(first_name)
   last = String(last_name)

   return Failure('The name must be 3 characters') if first.size <= 2

   value = yield normalize_str(first, last)
   result = yield resolve_name(value)

   Success(result)
 end

 private

 def normalize_str(first, last)
   Success([first.strip!, last.strip!].map(&:downcase))
 end

 def resolve_name(arr_names)
   Success(arr_names.join(' '))
 end
end

result = ResolveFullName.new

p result.call(first_name: '  aristoteles', last_name: 'COUTINHO ')
# Success("aristóteles coutinho")

A classe ResolveFullName recebe um input com first_name and last_name e irá retornar o nome completo de um possível usuário. Nesse ponto você percebe o uso da mônada Either ou result que irá tratar os casos de Success ou Failure.

Agora, imagine que por algum motivo tenhamos que enviar um cupom de desconto para um determinado usuário, caso ele se inscreva em um canal ou uma newsletter que seja...

Para isso, precisaremos primeiro precisaremos "normalizar" o nome e sobrenome usando a classe ResolveFullName. Logo em seguida vamos enviar o cupom por email. Há só pra garantir, vamos registrar esses dados do input em um "sistema de logs".😉

Vamos escrever a representação da interface de envio de email e escrita de logs, apenas para tornar o exemplo executável.

Não se preocupe, vou deixar todos os código em um gist 😉

# promotion_mailer.rb

module PromotionMailer
extend Dry::Monads[:result]

  def self.call(name, email, subject = 'Tá na mão 👋 o seu cupom!!!')
    attrs = {
      to: email,
      name: name,
      subject: subject,
      content: '80% de desconto...'
    }

    p '✉️ sending email...'
    Success(attrs)
  end
end
# write_log.rb

module WriteLog
extend Dry::Monads[:result]

  def self.call(**args)
    puts 'Writing in the logs...'
    puts args
    puts '🎉 Done!'
    Success('Done!')
  end
end

Usando dry-monads você poderia ter algo como:

# whatever_controller.rb
# ...
# ...

result = ResolveFullName.new

result
.call(first_name: '  aristoteles', last_name: 'COUTINHO ')
.bind(PromotionMailer, 'aristotelesbr@gmail.com')
.bind(WriteLog)

# "✉️ sending email..."
# Writing in the logs...
# {:to=>"aristotelesbr@gmail.com", :name=>"aristóteles coutinho", :subject => "Tá na mão 👋 o seu cupom!!!'", :content=>"Ofertas com 80% de desconto!"}
# 🎉 Done!

Perceba que o resultado de cada classe/módulo(ou até mesmo função) é passado como primeiro argumento para a próxima classe/módulo/função desde que a classe/módulo/função responda ao método .call.

Você pode pensar no seu pipeline da seguinte forma:

ROP-result

Seguindo do ponto "f" ao ponto "g", enquanto o resultado da mônada for o que esperadmos, ou seja, um "Success", a execução continuará pela representação do caminho em verde. Perceba que também é possível chegar do ponto "f" ao ponto "g" caso a execução falhe entre esses dois pontos.

Detalhando a execução do pipeline

  1. A classe ResolveFullName recebe como input o primeiro nome e o último nome e retorna o nome completo formatado de acordo com a regra de negócio.
  2. O módulo PromotionMailer recebe como primeiro argumento o retorno de ResolveFullName, como a aridade do método .call é call/2, ou seja, esse método recebe dois argumentos, estou passando em seguida o email aristótelesbr@gmail.com. PromotionMailer retorna uma hash com alguns atributos.
  3. O módulo WriteLog recebe a hash que foi retornada de PromotionMailer e como a aridade está sendo respeitada eu apenas passo o nome do módulo para a função bind.
result
.call(first_name: '  aristoteles', last_name: 'COUTINHO ') # 1
.bind(PromotionMailer, 'aristotelesbr@gmail.com')          # 2
.bind(WriteLog)                                            # 3

Tranquilo não acha? 😎

Conclusão

O dry-monads é uma excelente ferramenta. Fica muito tranquilo trabalhar mais o uso da composição ao invés de herança. Eu vejo muitas vantagens e benefícios no uso dessa abordagem, como por exemplo, a escrita de um código mais modular e de baixo acoplamento. Mantendo classes, módulos ou funções puras, muito menores, com baixo nível de complexidade e com responsabilidades bem definidas.

Bom, espero ter te ajudado em algo e se ficou com alguma dúvida ou se tem algum feedback me fala aí! Vamos aprender juntos!

até a próxima!✌🏻

  • 👉🏻 Link do código usando nos exemplos.
  • 👉🏻 Se quiser saber mais sobre Mônadas, Functors e programação funcional recomendo este episódio do podcast Lambda3

Comentários