DEV Community

Cover image for Chain of Responsibility e ASP.Net Core
William Santos
William Santos

Posted on

Chain of Responsibility e ASP.Net Core

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.");
}
Enter fullscreen mode Exit fullscreen mode

Agora, você pode estar se perguntando: por quê está implementação é ingênua?

Por dois motivos:

  1. Quanto mais condições forem adicionadas à esta operação, maior o método vai se tornar.
  2. 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);
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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.");
    }
}
Enter fullscreen mode Exit fullscreen mode

Repare em dois detalhes importantes na implementação acima:

  1. O handler BillStorageHandler recebe uma instância de WithdrawHandler em seu construtor, e o guarda como o próximo da cadeia. Este é um detalhe importante porque injetar a interface IHandler<TRequest, TResult>, ou a classe abstrata HandlerBase<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.
  2. O handler WithdrawalHandler informa null como próximo handler da cadeia, e sempre retorna true em seu método ShouldHandle. Isso acontece porque ele é o último nó da cadeia. Fixando o retorno true em ShouldHandle 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(...));
Enter fullscreen mode Exit fullscreen mode

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>();
}
Enter fullscreen mode Exit fullscreen mode

Nota: repare que ao registrar o handler BillStorageHandler foi informada a interface IHandler<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);
}
Enter fullscreen mode Exit fullscreen mode

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!

Discussion (4)

Collapse
wellingtonjhn profile image
Wellington Nascimento

Sensacional William, parabéns!

Collapse
wsantosdev profile image
William Santos Author

Muito obrigado, Wellington!

Collapse
vmamore profile image
Vinícius Mamoré

Muito daora William, valeu pelo post!

Collapse
wsantosdev profile image
William Santos Author

Eu que agradeço o carinho de sempre, Vinicius.

Valeu!