Service Result: aprofundando service objects
Levantar exception pra 'e-mail já cadastrado' trata um resultado esperado do negócio como se fosse um bug — um objeto de resultado resolve isso.
O FinalizarPedido da lição de service objects devolvia só true/false — o controller sabia que falhou, mas não sabia por quê. É tentador resolver isso levantando uma exception customizada pra carregar a mensagem de erro. Isso é pior do que o problema original.
O anti-padrão: exception pra um fluxo esperado
class EmailJaCadastrado < StandardError; end
class CadastrarUsuario
def initialize(params)
@params = params
end
def call
raise EmailJaCadastrado if Usuario.exists?(email: @params[:email])
Usuario.create!(@params)
end
enddef create
usuario = CadastrarUsuario.new(usuario_params).call
render json: usuario, status: :created
rescue EmailJaCadastrado
render json: { error: "E-mail já cadastrado" }, status: :unprocessable_entity
end"E-mail já cadastrado" acontece o tempo todo no uso normal do sistema — não é uma falha inesperada do programa, é um resultado esperado da regra de negócio. Tratar isso como exception mistura, no mesmo rescue, coisas de gravidade completamente diferente: um usuário digitando um e-mail já usado e o banco de dados caindo no meio da chamada viram, pro código, o mesmo tipo de evento. E o método call não deixa claro, só pela assinatura, que quem chama precisa envolver isso num rescue.
O padrão correto: um objeto de resultado
class Resultado
attr_reader :erros, :dado
def initialize(sucesso:, dado: nil, erros: [])
@sucesso = sucesso
@dado = dado
@erros = erros
end
def sucesso?
@sucesso
end
endclass CadastrarUsuario
def initialize(params)
@params = params
end
def call
return Resultado.new(sucesso: false, erros: ["E-mail já cadastrado"]) if Usuario.exists?(email: @params[:email])
usuario = Usuario.new(@params)
if usuario.save
Resultado.new(sucesso: true, dado: usuario)
else
Resultado.new(sucesso: false, erros: usuario.errors.full_messages)
end
end
enddef create
resultado = CadastrarUsuario.new(usuario_params).call
if resultado.sucesso?
render json: resultado.dado, status: :created
else
render json: { errors: resultado.erros }, status: :unprocessable_entity
end
endRepare: "e-mail já cadastrado" e "senha muito curta" (vindo de usuario.errors) agora passam pelo mesmo caminho — sucesso? falso, com a mensagem certa dentro de erros. Uma falha de verdade (o banco caindo no meio da chamada) continua subindo como exception normal e é tratada pelo rescue_from central — a diferença é que ela não é mais usada pra representar um resultado que o negócio já esperava.
Exercício
Reescreva o CancelarPedido (que reverte o status pra "cancelado" e devolve o estoque, mas só funciona se o pedido não tiver sido entregue) pra devolver um Resultado em vez de true/false, com a mensagem "Pedido já entregue não pode ser cancelado" no caso de falha.