Olá!
Este é mais um post da seção CooperaSharp e, desta vez, vamos examinar o desafio sobre explosão de dependências.
Vamos lá!
O Problema
Como é possível ver neste post temos um serviço com diversas dependências injetadas, o que cria o que chamamos de explosão de dependências no construtor (constructor dependency explosion em inglês).
Este serviço executa uma série de passos para tornar possível a criação de um pedido.
Esta abordagem é ruim porque, além de já conter inúmeras dependências, ainda pode adicionar mais caso o processo de torne maior, o que levaria a uma explosão ainda maior, tornando o construtor quase um container de dependências.
Aqui o trecho do código ruim:
public class PedidoService
{
private readonly IClienteRepository _clienteRepository;
private readonly IPedidoRepository _pedidoRepository;
private readonly IEstoqueService _estoqueService;
private readonly IPagamentoService _pagamentoService;
private readonly IEmailService _emailService;
private readonly INotificacaoService _notificacaoService;
private readonly ILogger _logger;
private readonly IFreteService _freteService;
private readonly IMapper _mapper;
private readonly IAuditoriaService _auditoriaService;
public PedidoService(
IClienteRepository clienteRepository,
IPedidoRepository pedidoRepository,
IEstoqueService estoqueService,
IPagamentoService pagamentoService,
IEmailService emailService,
INotificacaoService notificacaoService,
ILogger logger,
IFreteService freteService,
IMapper mapper,
IAuditoriaService auditoriaService)
{
_clienteRepository = clienteRepository;
_pedidoRepository = pedidoRepository;
_estoqueService = estoqueService;
_pagamentoService = pagamentoService;
_emailService = emailService;
_notificacaoService = notificacaoService;
_logger = logger;
_freteService = freteService;
_mapper = mapper;
_auditoriaService = auditoriaService;
}
public void ProcessarPedido(PedidoDto dto)
{
var cliente = _clienteRepository.ObterPorId(dto.ClienteId);
if (cliente == null)
{
_logger.Log("Cliente não encontrado");
return;
}
var pedido = _mapper.Map<Pedido>(dto);
_estoqueService.ReservarItens(pedido.Itens);
var valorFrete = _freteService.CalcularFrete(dto.EnderecoEntrega);
pedido.AdicionarFrete(valorFrete);
var pagamentoOk = _pagamentoService.Processar(pedido);
if (!pagamentoOk)
{
_notificacaoService.Enviar("Falha no pagamento");
return;
}
_pedidoRepository.Salvar(pedido);
_emailService.EnviarConfirmacao(cliente.Email);
_auditoriaService.Registrar("Pedido criado", pedido.Id.ToString());
}
}
Uma Solução Ingênua
Uma das soluções que pode vir à mente neste momento é a de trabalhar com eventos e deixar cada handler se prontificar a resolver uma parte do processo, fazendo com que o PedidoService cuide apenas da persistência do pedido.
Seria uma solução interessante se não fosse por um detalhe: repare neste trecho:
_estoqueService.ReservarItens(pedido.Itens);
var valorFrete = _freteService.CalcularFrete(dto.EnderecoEntrega);
pedido.AdicionarFrete(valorFrete);
O que acontece se houver uma exceção no serviço de frete? Pois é! O o estoque permanece com os itens reservados ainda que o processo no geral falhe. Isso leva a uma inconsistência no estado da aplicação, podendo impedir que outro pedido seja feito por falta de estoque.
Portanto, precisamos pensar numa forma de tornar possível que ações compensatórias como a liberação dos itens seja possível. E aí entra a solução que encontrei para o desafio.
Minha Solução (Elegante?)
Se pensarmos bem, este processo todo é uma transação. Se um passo dela falhar, todas devem falhar, executando um rollback e revertendo a mudança de estado que eventualmente tenha provocado. Então entra em cena um mecanismo de transação em cadeia, inspirado no pattern Chain of Responsibility sobre o qual falo neste post.
Vamos conhecer o código!
Em primeiro lugar, precisamos ser capazes de descrever os passos da nossa transação e, para isso, foram criados os TransactionSteps, como podemos ver abaixo:
public interface ITransactionStep<TInput> where TInput : notnull
{
Result<Error> Execute(ref TInput input);
Result<Error> Rollback(Result<Error> error);
}
public abstract class TransactionStepBase<TInput>(ITransactionStep<TInput>? next) : ITransactionStep<TInput> where TInput : notnull
{
public Result<Error> Execute(ref TInput input)
{
var result = ExecuteInternal(ref input);
if(result && next is not null)
return next.Execute(ref input);
if(result && next is null)
return result;
return Rollback(result);
}
protected abstract Result<Error> ExecuteInternal(ref TInput input);
public abstract Result<Error> Rollback(Result<Error> error);
}
Aqui temos uma interface que vai demarcar a anatomia de um passo transacional e, também, uma classe abstrata que deverá ser implementada por cada passo.
Repare que cada passo da transação, tal qual na cadeia de responsabilidades, recebe uma referência para o passo seguinte. A diferença está no método Rollback
, que será invocado caso hava um resultado negativo do passo implementação.
Há, também, uma verificação do próximo passo a fim de saber se alcançamos o fim da cadeia.
Agora vamos ver a implementação de um dos passos, o de verificação da existência de um cliente:
public sealed class CustomerValidationStep(CustomerDataAccess customerDataAccess,
OrderStockReservationStep next) : TransactionStepBase<OrderSubmissionBag>(next)
{
protected override Result<Error> ExecuteInternal(ref OrderSubmissionBag input)
{
try
{
var customer = customerDataAccess.GetById(input.Request.CustomerId);
if (customer)
{
input.Customer = customer.Get();
return Result<Error>.Ok();
}
return Result<Error>.Error(new Error($"Customer with id {input.Request.CustomerId} not found"));
}
catch(Exception ex)
{
return Rollback(new Error(ex.Message));
}
}
public override Result<Error> Rollback (Result<Error> error) =>
error;
}
Repare que aqui temos três possíveis desfechos para a execução deste passo:
- O cliente existe e é incluído em uma
OrderSubmissionBag
que contém uma referência para a requisição recebida pelo servidor, uma para o cliente e outra para o pedido. Neste desfecho o passo seguinte é executado, oOrderStockReservationStep
, responsável por reservar os itens do pedido no estoque. - O cliente não existe e é retornado um erro indicando que o Id informado não foi encontrado.
- Há uma exceção em algum ponto do fluxo e o método
Rollback
é acionado. Neste caso, por tratar-se de uma consulta, não há a necessidade de reverter o estado da aplicação, portanto apenas um erro será retornado, interrompendo a execução da cadeia transacional.
Para iniciar a execução destes passos, é necessário um transactor, que funciona apenas como um iniciador do processo.
Veja o código abaixo:
public sealed class OrderSubmissionTransactor(CustomerValidationStep step)
{
public Result<Error> Process(OrderSubmissionBag bag) =>
step.Execute(ref bag);
}
Com isso, como pode ser visto no repositório da solução, todos os passos da transação podem ser executados em sequência e novos podem ser encadeados, injetando apenas a dependência do Transactor no Controller, como segue:
[Route("api/[controller]")]
[ApiController]
public class SubmitOrderController(OrderSubmissionTransactor transactor) : ControllerBase
{
[HttpPost]
public IActionResult SubmitOrder([FromBody] OrderSubmissionRequest request)
{
var result = transactor.Process(new OrderSubmissionBag { Request = request, Customer = null, Order = null });
return result
? Ok()
: StatusCode(500, "It was not possible to submit your order. Try again later.");
}
}
Conclusão
Evitar a explosão de dependências é interessante para manter o código legível e, também, evitar o que alguns chamam de God Classes ou Megazords, classes que respondem por muitas atividades e cuja legibilidade geralmente é baixa, assim como é sua testabilidade - imagine criar e injetar 10 instâncias reais para testar esta classe, ou pior, utilizar mocks para isso!
Com a solução que proponho, cada passo do processo está autocontido, portanto coeso, não tem conhecimento dos demais, exceto pelo passo imediatamente seguinte, eliminamos a explosão de dependências e, também, mantemos a consistência do estado da aplicação.
Aparentemente uma vitória!
Gostou? Me deixe saber pelos indicadores ou por minhas redes sociais.
Muito obrigado por ler até aqui, e até o próximo post!
Top comments (0)