Ruby on Rails modularização com Packwerk Episódio I
Os seres humanos têm dificuldade em ver o panorama geral de um problema sem dedicar muito tempo e esforço. Isto acontece especialmente quando se trabalha com aplicações grandes e complexas....
Um problema comum ao trabalhar com Rails é decidir onde colocar a lógica das nossas funcionalidades.
A lógica é frequentemente colocada nos controladores, modelos ou, se tivermos sorte, num objeto de serviço. Então, se temos Objectos de Serviço, porque é que precisamos de Casos de Utilização?
Siga-me neste artigo para descobrir os benefícios deste padrão.
Um caso de utilização é uma lista de acções ou etapas de eventos que normalmente definem as interações entre uma função e um sistema para atingir um objetivo.
Vale a pena mencionar que este padrão é aplicado de muitas formas diferentes e tem nomes alternativos. Podemos encontrá-lo como Interactores, Operadores ou Comandos, mas no Rubi comunidade com a qual nos mantemos Caso de utilização. Cada implementação é diferente, mas com o mesmo objetivo: servir o caso de utilização do sistema por um utilizador.
Mesmo que na nossa projeto não estamos a definir os requisitos utilizando Caso de utilizaçãos e UML, este padrão continua a ser útil para estruturar a lógica empresarial de uma forma prática.
O nosso Casos de utilização deve ser:
Vejamos o exemplo de um utilizador que pretende comprar algo no nosso sistema.
módulo UseCases
módulo Comprador
classe Compra
def initialize(comprador:, carrinho:)
@comprador = comprador
@carrinho = carrinho
fim
def call
return unless check_stock
return unless create_purchase
notificar end
privado
attr_reader :comprador, :carrinho
def check_stock
Services::CheckStock.call(carrinho: carrinho)
fim
def criar_compra
Services::CreatePurchase.call(buyer: buyer, cart: cart).call
end
def notify
Services::NotifyBuyer.call(buyer: buyer)
end
end
end
fim
Como pode ver neste código exemplo, criámos um novo Caso de utilização chamado Purchase. Definimos apenas um método público chamada. Dentro do método call, encontramos passos bastante básicos para efetuar uma compra, e todos os passos são definidos como métodos privados. Cada passo está a chamar um Objeto de Serviço, desta forma o nosso Caso de utilização está apenas a definir os passos para efetuar uma compra e não a lógica em si. Isto dá nós uma imagem clara do que pode ser feito no nosso sistema (efetuar uma compra) e os passos para o conseguir.
Agora estamos prontos para chamar o nosso primeiro Caso de utilização de um controlador.
classe Controlador
def purchase
UseCases::Buyer::Purchase.new(
comprador: purchase_params[:buyer],
carrinho: purchase_params[:carrinho]
).call
...
fim
...
fim
Nesta perspetiva, o Caso de utilização é muito parecido com um Objeto de Serviço, mas o seu objetivo é diferente. Um Objeto de Serviço realiza uma tarefa de baixo nível e interage com diferentes partes do sistema, como a Base de Dados, enquanto o Caso de utilização cria uma nova abstração de alto nível e define as etapas lógicas.
O nosso primeiro Caso de utilização funciona mas podia ser melhor. Como é que o podemos melhorar? Vamos utilizar o seco gemas. Neste caso, vamos utilizar transação seca.
Primeiro, vamos definir a nossa classe base.
classe UseCase
include Dry::Transaction
class << self
def call(**args)
new.call(**args)
fim
fim
fim
Isto ajudar-nos-á a passar atributos para a transação UseCase e a utilizá-los. Em seguida, estamos prontos para redefinir o nosso Caso de Utilização de Compra.
módulo UseCases
módulo Comprador
classe Compra
def initialize(comprador:, carrinho:)
@comprador = comprador
@carrinho = carrinho
fim
def call
return unless check_stock
return unless create_purchase
notificar
fim
privado
attr_reader :comprador, :carrinho
def check_stock
Serviços::CheckStock.call(carrinho: carrinho)
fim
def create_purchase
Services::CreatePurchase.call(buyer: buyer, cart: cart).call
fim
def notify
Services::NotifyBuyer.call(buyer: buyer)
end
end
end
fim
Com as novas alterações, podemos ver claramente como as nossas etapas são definidas e podemos gerir o resultado de cada etapa com Success() e Failure().
Estamos prontos para chamar o nosso novo caso de utilização no controlador e preparar a nossa resposta em função do resultado final.
classe Controlador
def purchase
UseCases::Buyer::Purchase.new.call(
comprador: purchase_params[:buyer],
carrinho: purchase_params[:carrinho]
) do |result|
result.success do
...
fim
result.failure do
...
end
fim
...
fim
...
fim
Este exemplo poderia ser melhorado ainda mais com validações, mas é suficiente para mostrar o poder deste padrão.
Sejamos honestos, o Padrão de caso de utilização é bastante simples e parece-se muito com um Objeto de Serviço, mas este nível de abstração pode fazer uma grande mudança na sua aplicação.
Imagine que um novo programador se junta ao projeto e abre a pasta use_cases, como primeira impressão terá uma lista de todas as funcionalidades disponíveis no sistema e, depois de abrir um caso de utilização, verá todos os passos necessários para essa funcionalidade sem se aprofundar na lógica. Esta sensação de ordem e controlo é a principal vantagem deste padrão.
Leve isto na sua caixa de ferramentas e talvez no futuro lhe dê uma boa utilização.
