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 |
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 |
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, |
Repare que aqui temos três possíveis desfechos para a execução deste passo:
-
O cliente existe e é incluído em uma
OrderSubmissionBagque 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]")] |
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.




