Olá!
Este é mais um post da seção Design, e nele vamos tratar de um pattern bastante útil em cenários com múltiplas condições, o Chain of Responsibility (CoR, ou Cadeia de Responsabilidade, em tradução livre). Veremos também como integrá-lo ao container de injeção de dependência do ASP.Net Core.
Vamos lá!
O Problema
Antes de mais nada, precisamos entender qual a utilidade do pattern, ou seja, qual problema ele resolve. Patterns são soluções cabíveis para um dado tipo de problema, e com o CoR não é diferente.
Imagine um cenário onde, para atender a uma dada requisição (ou comando) a satisfação de diversas condições seja necessária e que, para cada condição, pode haver um dado processamento específico a ser realizado ou um tipo de resultado a ser retornado.
Soa estranho? Explico.
Vamos imaginar um caixa eletrônico e sua função de saque. Para permitir o saque, o caixa eletrônico precisa validar se há saldo em conta disponível, se há o montante solicitado disponível no compartimento de notas, se há alguma limitação no valor do saque por horário etc.
Uma implementação ingênua seria mais ou menos assim:
public (bool, string) Withdraw(WithdrawalRequest request)
{
if(request.Amount == 0)
return (false, "Please fill a valid positive amount to withdraw.");
var account = _accountRepository.Get(request.AccountNumber);
if(!account.HasAmount(request.Amount))
return (false, "There is not enough balance for this withdraw.");
if(!_billStorage.HasAmount(request.Amount))
return (false, "There aren't enough bills for this withdraw.");
if(_withdrawRestrictionService.ShouldRestrictWithdraw(request.Amount, DateTime.Now))
(false, "The amount informed is greater than allowed at this time.");
_billStorage.Withdraw(request.Amount);
return (true, "Sucessful withdrawal.");
}
Agora, você pode estar se perguntando: por quê está implementação é ingênua?
Por dois motivos:
- Quanto mais condições forem adicionadas à esta operação, maior o método vai se tornar.
- Quanto mais dependências forem necessárias para atender a estas condições, maior será a carga cognitiva para lidar com todas elas.
Vejamos a seguir como o CoR pode nos ajudar a lidar com estas questões.
O Pattern
O pattern sugere que, para cada condição a ser atendida para uma requisição ou comando, tenhamos um handler, um tipo responsável por validá-la, e que este contenha uma referência a outro handler, que será o próximo da cadeia, para encaminhar esta requisição caso não haja razão para interceptá-la e tratá-la.
Nota: neste post, sugiro uma abordagem diferente da canônica para a aplicação do pattern. Um exemplo da abordagem canônica pode ser encontrado no Refactoring Guru (em inglês).
Como precisaremos de um handler para cada condição, e todos estão sujeitos ao mesmo procedimento, ou seja, recebem a mesma requisição e retornam um mesmo tipo de resultado, podemos estabelecer um contrato que represente este comportamento. Vejamos abaixo:
public interface IHandler<TRequest, TResult>
{
public bool ShouldHandle(TRequest request);
public TResult Handle(TRequest request);
}
Aqui temos dois métodos: um que vai verificar se o handler em questão deve interceptar a requisição recebida; e outro que manipula a requisição de fato, interceptando-a.
Nota: uma abordagem alternativa é tornar os dois métodos assíncronos, em uma segunda interface chamada IAsyncHandler, e por um bom motivo: nem sempre o que vai determinar se a requisição deve ou não ser interceptada depende da validação de seu próprio estado. Há situações onde uma operação, como um I/O, precisa acontecer para fazer esta verificação e, para estes casos, um método assíncrono é muito bem-vindo!
Com estes dois métodos, atendemos à primeira porção do pattern, que cada handler saiba se é responsável ou não por interceptar e tratar uma dada requisição e, em caso positivo, que a manipule em seguida.
Agora precisamos atender à segunda porção, precisamos guardar uma referência para o próximo handler, e garantir que todos os handlers que implementarmos seguirão a mesma lógica de verificação e manipulação. Para isso, vamos usar uma classe abstrata que implementa nosso contrato:
public abstract class HandlerBase<TRequest, TResponse> : IHandler<TRequest, TResponse>
{
private readonly IHandler<TRequest, TResponse> _next;
public HandlerBase(IHandler<TRequest, TResponse> next) =>
_next = next;
public abstract bool ShouldHandle(TRequest request);
public TResponse Handle(TRequest request)
{
if(ShouldHandle(request))
return HandleCore(request);
return _next.Handle(request);
}
protected abstract TResponse HandleCore(TRequest request);
}
Agora temos garantido o seguinte comportamento: se a requisição puder ser manipulada pelo handler atual, ela o será. Caso contrário, será encaminhada ao handler seguinte.
Com isso, podemos implementar um handler para cada condição de nosso método de saque. Vamos a um exemplo:
public class BillStorageHandler : HandlerBase<WithdrawalRequest, WithdrawalResult>
{
private readonly BillStorage _billStorage;
public BillStorageHandler(WithdrawHandler next,
BillStorage billStorage) : base(next)
{
public override bool ShouldHandle(WithdrawalRequest request) =>
!_billStorage.HasAmount(request.Amount);
protected override WithdrawalResult HandleCore(WithdrawalRequest request) =>
WithdrawalResult.Fail("There aren't enough bills for this withdrawal.");
}
}
...
public class WithdrawHandler : HandlerBase<WithdrawalRequest, WithdrawalResult>
{
private readonly BillStorage _billStorage;
public WithdrawHandler(BillStorage billStorage) : base(null) =>
_billStorage = billStorage;
public override bool ShouldHandle => true;
protected override WithdrawalResult HandleCore(WithdrawalRequest request)
{
_billStorage.Withdraw(request.Amount);
return WithdrawalResult.Ok("Successful withdrawal.");
}
}
Repare em dois detalhes importantes na implementação acima:
- O handler
BillStorageHandler
recebe uma instância deWithdrawHandler
em seu construtor, e o guarda como o próximo da cadeia. Este é um detalhe importante porque injetar a interfaceIHandler<TRequest, TResult>
, ou a classe abstrataHandlerBase<TRequest, TResult>
, além de mais verboso, impede a identificação do próximo handler da cadeia. Recebendo a especialização por injeção, fica mais claro qual é o próximo passo caso a requisição não deva ser manipulada por este handler. - O handler
WithdrawalHandler
informanull
como próximo handler da cadeia, e sempre retornatrue
em seu métodoShouldHandle
. Isso acontece porque ele é o último nó da cadeia. Fixando o retornotrue
emShouldHandle
há a garantia de que a requisição sempre receberá um tratamento ao final da cadeia.
Injeção de Dependência
Aqui precisamos falar sobre a abordagem canônica do pattern e o motivo pelo qual ela foi evitada neste post. A abordagem canônica sugere que na interface IHandler<TRequest, TResponse>
haja um método chamado SetNext
, onde seria passada por parâmetro a instância do próximo handler, permitindo assim a seguinte declaração:
var billStorageHandler = new BillStorageHandler(...);
billStorageHandler.SetNext(new WithdrawHandler(...));
O problema com esta abordagem é que a inversão de controle é inviabilizada, e qualquer dependência de quaisquer dos handlers precisariam ser instanciadas a priori de sua criação, impedindo os ganhos oferecidos pelo contêiner de injeção de dependência.
Com a abordagem proposta neste post, a declaração se torna bastante simplificada, como o seguinte exemplo:
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<BillStorage>()
.AddScoped<IHandler<WithdrawalRequest, WithdrawalResult>, BillStorageHandler>()
.AddScoped<WithdrawalHandler>();
}
Nota: repare que ao registrar o handler
BillStorageHandler
foi informada a interfaceIHandler<TRequest, TResult>
. Essa declaração, opcional, é uma forma de anonimizar o primeiro handler na classe onde a cadeia será invocada, se desejado. Desta forma, caso o primeiro handler da cadeia precise ser substituído, não haverá a necessidade de se modificar a classe que consumirá a cadeia.
Com isso temos todas as nossas dependências registradas e podemos refatorar nosso método de saque:
public class WithdrawalProcessor
{
private readonly IHandler<WithdrawalRequest, WithdrawalResult> _handler;
public WithdrawalProcessor(IHandler<WithdrawalRequest, WithdrawalResult> handler) =>
_handler = handler;
public WithdrawalResult Withdraw(WithdrawalRequest request) =>
_handler.Handle(request);
}
Muito mais simples. Não? Não há mais uma sequência potencialmente infinita de condicionais, as dependências agora são injetadas em cada handler, deixando nosso processador de requisições mais leve e limpo, e o código foi bastante enxugado, tornando sua compreensão e manutenção mais simples.
Conclusão
O Chain of Responsibility torna muito mais simples lidar com situações que demandam múltiplas condicionais, e que podem, ou não, resumir o fluxo de uma dada requisição ou comando. É um acessório muito útil e que pode ser usado em diversas situações, desde validações a execução de procedimentos.
Gostou? Me deixe saber pelos comentários ou por minhas redes sociais.
Muito obrigado pela leitura, e até a próxima!
Top comments (4)
Sensacional William, parabéns!
Muito obrigado, Wellington!
Muito daora William, valeu pelo post!
Eu que agradeço o carinho de sempre, Vinicius.
Valeu!