Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
November 22, 2021 12:19 am GMT

Chain of Responsibility e ASP.Net Core

Ol!

Este mais um post da seo Design, e nele vamos tratar de um pattern bastante til em cenrios com mltiplas condies, o Chain of Responsibility (CoR, ou Cadeia de Responsabilidade, em traduo livre). Veremos tambm como integr-lo ao container de injeo de dependncia 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 so solues cabveis para um dado tipo de problema, e com o CoR no diferente.

Imagine um cenrio onde, para atender a uma dada requisio (ou comando) a satisfao de diversas condies seja necessria e que, para cada condio, pode haver um dado processamento especfico a ser realizado ou um tipo de resultado a ser retornado.

Soa estranho? Explico.

Vamos imaginar um caixa eletrnico e sua funo de saque. Para permitir o saque, o caixa eletrnico precisa validar se h saldo em conta disponvel, se h o montante solicitado disponvel no compartimento de notas, se h alguma limitao no valor do saque por horrio etc.

Uma implementao ingnua 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 implementao ingnua?

Por dois motivos:

  1. Quanto mais condies forem adicionadas esta operao, maior o mtodo vai se tornar.
  2. Quanto mais dependncias forem necessrias para atender a estas condies, maior ser a carga cognitiva para lidar com todas elas.

Vejamos a seguir como o CoR pode nos ajudar a lidar com estas questes.

O Pattern

O pattern sugere que, para cada condio a ser atendida para uma requisio ou comando, tenhamos um handler, um tipo responsvel por valid-la, e que este contenha uma referncia a outro handler, que ser o prximo da cadeia, para encaminhar esta requisio caso no haja razo para intercept-la e trat-la.

Nota: neste post, sugiro uma abordagem diferente da cannica para a aplicao do pattern. Um exemplo da abordagem cannica pode ser encontrado no Refactoring Guru (em ingls).

Como precisaremos de um handler para cada condio, e todos esto sujeitos ao mesmo procedimento, ou seja, recebem a mesma requisio 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 mtodos: um que vai verificar se o handler em questo deve interceptar a requisio recebida; e outro que manipula a requisio de fato, interceptando-a.

Nota: uma abordagem alternativa tornar os dois mtodos assncronos, em uma segunda interface chamada IAsyncHandler, e por um bom motivo: nem sempre o que vai determinar se a requisio deve ou no ser interceptada depende da validao de seu prprio estado. H situaes onde uma operao, como um I/O, precisa acontecer para fazer esta verificao e, para estes casos, um mtodo assncrono muito bem-vindo!

Com estes dois mtodos, atendemos primeira poro do pattern, que cada handler saiba se responsvel ou no por interceptar e tratar uma dada requisio e, em caso positivo, que a manipule em seguida.

Agora precisamos atender segunda poro, precisamos guardar uma referncia para o prximo handler, e garantir que todos os handlers que implementarmos seguiro a mesma lgica de verificao e manipulao. 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 requisio puder ser manipulada pelo handler atual, ela o ser. Caso contrrio, ser encaminhada ao handler seguinte.

Com isso, podemos implementar um handler para cada condio de nosso mtodo 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 implementao acima:

  1. O handler BillStorageHandler recebe uma instncia de WithdrawHandler em seu construtor, e o guarda como o prximo da cadeia. Este um detalhe importante porque injetar a interface IHandler<TRequest, TResult>, ou a classe abstrata HandlerBase<TRequest, TResult>, alm de mais verboso, impede a identificao do prximo handler da cadeia. Recebendo a especializao por injeo, fica mais claro qual o prximo passo caso a requisio no deva ser manipulada por este handler.
  2. O handler WithdrawalHandler informa null como prximo handler da cadeia, e sempre retorna true em seu mtodo ShouldHandle. Isso acontece porque ele o ltimo n da cadeia. Fixando o retorno true em ShouldHandle h a garantia de que a requisio sempre receber um tratamento ao final da cadeia.

Injeo de Dependncia

Aqui precisamos falar sobre a abordagem cannica do pattern e o motivo pelo qual ela foi evitada neste post. A abordagem cannica sugere que na interface IHandler<TRequest, TResponse> haja um mtodo chamado SetNext, onde seria passada por parmetro a instncia do prximo handler, permitindo assim a seguinte declarao:

var billStorageHandler = new BillStorageHandler(...);billStorageHandler.SetNext(new WithdrawHandler(...));

O problema com esta abordagem que a inverso de controle inviabilizada, e qualquer dependncia de quaisquer dos handlers precisariam ser instanciadas a priori de sua criao, impedindo os ganhos oferecidos pelo continer de injeo de dependncia.

Com a abordagem proposta neste post, a declarao 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 declarao, 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 substitudo, no haver a necessidade de se modificar a classe que consumir a cadeia.

Com isso temos todas as nossas dependncias registradas e podemos refatorar nosso mtodo 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. No? No h mais uma sequncia potencialmente infinita de condicionais, as dependncias agora so injetadas em cada handler, deixando nosso processador de requisies mais leve e limpo, e o cdigo foi bastante enxugado, tornando sua compreenso e manuteno mais simples.

Concluso

O Chain of Responsibility torna muito mais simples lidar com situaes que demandam mltiplas condicionais, e que podem, ou no, resumir o fluxo de uma dada requisio ou comando. um acessrio muito til e que pode ser usado em diversas situaes, desde validaes a execuo de procedimentos.

Gostou? Me deixe saber pelos comentrios ou por minhas redes sociais.

Muito obrigado pela leitura, e at a prxima!


Original Link: https://dev.to/wsantosdev/chain-of-responsibility-e-aspnet-core-14hn

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To