An Interest In:
Web News this Week
- March 20, 2024
- March 19, 2024
- March 18, 2024
- March 17, 2024
- March 16, 2024
- March 15, 2024
- March 14, 2024
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:
- Quanto mais condies forem adicionadas esta operao, maior o mtodo vai se tornar.
- 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:
- O handler
BillStorageHandler
recebe uma instncia deWithdrawHandler
em seu construtor, e o guarda como o prximo da cadeia. Este um detalhe importante porque injetar a interfaceIHandler<TRequest, TResult>
, ou a classe abstrataHandlerBase<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. - O handler
WithdrawalHandler
informanull
como prximo handler da cadeia, e sempre retornatrue
em seu mtodoShouldHandle
. Isso acontece porque ele o ltimo n da cadeia. Fixando o retornotrue
emShouldHandle
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 interfaceIHandler<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
Dev To
An online community for sharing and discovering great ideas, having debates, and making friendsMore About this Source Visit Dev To