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 contain…


This content originally appeared on DEV Community and was authored by William Santos

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:

  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);
}

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:

  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(...));

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 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);
}

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!


This content originally appeared on DEV Community and was authored by William Santos


Print Share Comment Cite Upload Translate Updates
APA

William Santos | Sciencx (2021-11-22T00:19:15+00:00) Chain of Responsibility e ASP.Net Core. Retrieved from https://www.scien.cx/2021/11/22/chain-of-responsibility-e-asp-net-core/

MLA
" » Chain of Responsibility e ASP.Net Core." William Santos | Sciencx - Monday November 22, 2021, https://www.scien.cx/2021/11/22/chain-of-responsibility-e-asp-net-core/
HARVARD
William Santos | Sciencx Monday November 22, 2021 » Chain of Responsibility e ASP.Net Core., viewed ,<https://www.scien.cx/2021/11/22/chain-of-responsibility-e-asp-net-core/>
VANCOUVER
William Santos | Sciencx - » Chain of Responsibility e ASP.Net Core. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2021/11/22/chain-of-responsibility-e-asp-net-core/
CHICAGO
" » Chain of Responsibility e ASP.Net Core." William Santos | Sciencx - Accessed . https://www.scien.cx/2021/11/22/chain-of-responsibility-e-asp-net-core/
IEEE
" » Chain of Responsibility e ASP.Net Core." William Santos | Sciencx [Online]. Available: https://www.scien.cx/2021/11/22/chain-of-responsibility-e-asp-net-core/. [Accessed: ]
rf:citation
» Chain of Responsibility e ASP.Net Core | William Santos | Sciencx | https://www.scien.cx/2021/11/22/chain-of-responsibility-e-asp-net-core/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.