ASP.NET Core - Apresentando princípios de Clean Architecture e de CRQS


  Hoje vamos apresentar maneiras de melhorar o código de um aplicativo combinando os princípios das abordagens Clean Architecture e Command and Query Responsibility Segregation (CQRS).

A Clean Architecture (Arquitetura Limpa) é uma abordagem de arquitetura de software que foi proposta por Robert C. Martin, também conhecido como Uncle Bob, em seu livro "Clean Architecture: A Craftsman's Guide to Software Structure and Design".


A ideia central da Clean Architecture é criar um sistema que seja fácil de entender, de manter e de modificar ao longo do tempo, e que seja independente de tecnologias específicas ou de detalhes de implementação.

Em termos gerais, a Clean Architecture consiste em dividir o sistema em camadas ou anéis concêntricos, cada um com uma responsabilidade bem definida e restrita. As camadas mais internas são as mais importantes e representam as regras de negócio do sistema, enquanto as camadas mais externas lidam com aspectos técnicos como interfaces de usuário, bancos de dados e frameworks. Essa abordagem permite que as mudanças em uma camada afetem minimamente as outras camadas, facilitando a manutenção e evolução do sistema.

Explorando a arquitetura limpa e o CQRS

Aplicativos com seções de código altamente interdependentes são difíceis de aprimorar e oferecer suporte. Uma razão para isso é que o código fica confuso com o tempo e a menor alteração pode levar a consequências devastadoras, incluindo falhas de aplicativos e violações de segurança. Para melhorar a qualidade e a capacidade de manutenção do código do seu aplicativo, você pode dividi-lo em camadas separadas e independentes que trabalham com a interface do usuário (UI), lógica de negócios e dados.

Você pode seguir os princípios de Clean Architecture e Command and Query Responsibility Segregation (CQRS) para garantir que as áreas de código responsáveis pela interface do usuário, lógica de negócios e dados não dependam umas das outras e permitam uma rápida introdução de alterações.

A abordagem da Arquitetura requer a divisão do código do aplicativo em várias camadas independentes:

- Entidades de domínio
- Casos de uso
- Ccontroladores
- Bancos de dados
- Interface de usuário
- Serviços externos


Essa separação de preocupações pode ajudá-lo a criar uma solução flexível, fácil de testar e sem dependências de estrutura, bancos de dados e serviços externos. Assim podemos destacar as seguintes características da Clean Architecture:

  1. Nenhuma dependência de frameworks
  2. Permite testar a lógica de negócio separada dos elementos externos
  3. Permite alterar a camada de apresentação sem ter que mudar o sistema
  4. Nenhuma dependência de banco de dados
  5. Independência de serviços externos

No entanto, para garantir a máxima clareza de código, especialmente em projetos longos e complexos com muito código confuso, sugerimos combinar os princípios da Clean Architecture com a abordagem CQRS, que visa separar as operações de leitura de dados das operações de gravação.

Ao adicionar uma camada extra ao seu aplicativo, a abordagem CQRS traz vários benefícios significativos como:

  1. Separação das responsabilidades : Comandos e consultas no código do seu aplicativo só podem ter uma tarefa: recuperar ou alterar dados.
     
  2. Testabilidade :  A simplicidade do design torna seus manipuladores de comandos ou consultas fáceis de testar. Você não precisa escrever testes para manipuladores que não tenham serviço cruzado, módulo cruzado ou qualquer outra chamada cruzada.
     
  3. Escalabilidade : O padrão CQRS é flexível em termos de como você organiza seu armazenamento de dados. Você pode usar bancos de dados de leitura/gravação separados para comandos e consultas com mensagens ou replicação entre bancos de dados para sincronização.
     
  4. Desacoplamento :  Um comando ou consulta é completamente desacoplado de seu manipulador.

A CQRS e a Clean Architecture se encaixam perfeitamente, pois os fluxos de trabalho de domínio não sabem nada sobre a existência de comandos e consultas. Ao mesmo tempo, ainda podemos chamar quantos métodos de lógica de negócios forem necessários em um único comando ou consulta.

Vamos ver como podemos combinar essas duas abordagens para melhorar a coesão e a flexibilidade do código do aplicativo. Como exemplo, implementaremos a Clean Architecture em um aplicativo Web .NET Core.

Clean Architecture para aplicações .NET

Digamos que temos um aplicativo web processando pedidos online. Nosso objetivo é isolar o domínio, a infraestrutura e o banco de dados desse aplicativo para que possamos alterá-los independentemente um do outro. Para isso, criamos projetos separados para:

Podemos esquematizar isso da seguinte forma:

Projetando comandos e consultas com MediatR

A MediatR é uma biblioteca destinada a desacoplar as operações de envio de mensagens. Para trabalhar com MediatR, você precisará de um contêiner de injeção de dependência (DI).

No nosso projeto vamos usar o contêiner integrado .NET Core DI, pois seus recursos atendem aos requisitos do projeto. Tudo o que precisamos é instalar o pacote MediatR e chamar o método AddMediatR com o seguinte comando:


builder.Services.AddMediatR(cfg =>
    cfg.RegisterServicesFromAssembly(
typeof(Program).Assembly));

 

Existem dois tipos de mensagens na biblioteca MediatR:

request/response — Essa mensagem é processada por um manipulador, que geralmente retorna uma resposta.
notification — Esta mensagem pode ser despachada por vários manipuladores que não retornam nenhuma resposta.

Em nosso projeto, vamos usar apenas mensagens de request/response, então devemos ter várias classes implementando a interface IRequest<T>.

O trecho de código a seguir mostra um exemplo de uma consulta :

using MediatR;

namespace AppMediatR.Alunos.Queries;

public class PedidoQuery
{
  
public int PedidoNumero { get; set; }
  
public string Telefone { get; set; }

   IPedidoService _pedidoService;

   public PedidoQuery(IPedidoService pedidoService)
   {
       _pedidoService = pedidoService;
   }

   public class PedidoQueryHandler : IRequestHandler<PedidoQuery, IEnumerable<Pedido>>
   {
    
public async Task<IEnumerable<Pedido>> Handle(PedidoQuery request,
                                          CancellationToken cancellationToken)
     {
       
if (request.PedidoNumero > 0)
        {
         
return await _pedidoService.FindByNo(request.PedidoNumero, cancellationToken);
        }
       
return _pedidoService.FindOrders(request.Telefone, cancellationToken);
     }
   }
}

Note que colocamos a classe PedidoQueryHandler : IRequestHandler<PedidoQuery, string> dentro da classe de request PedidoQuery. Embora isso não seja obrigatório, usamos essa abordagem para encapsular os parâmetros e o manipulador na mesma classe.

Como as mensagens em nosso projeto são enviadas pelos controladores, deve haver um mediador responsável por chamar o manipulador necessário injetado em cada controlador:

public class PedidosController : Controller
{
  
private readonly IMediator _mediator;
  
public PedidosController(IMediator mediator)
   {
     _mediator = mediator;
   }
}

Depois que uma solicitação HTTP for mapeada para o método do controlador, o método envia um comando ou consulta encapsulada:

[HttpGet("find-pedidos")]
public Task<IEnumerable<Pedido>> FindPedidos(int pedidoNumero, string telefone) =>
    _mediator.Send(
new PedidoQuery()
    {
       PedidoNumero = pedidoNumero,
       Telefone = telefone
    });

Implementando log implícito com MediatR

O registro de log e a criação de perfil são processos absolutamente necessários para qualquer aplicativo moderno. Eles fornecem informações detalhadas sobre onde ocorreu um problema e os dados que o causaram, ajudando desenvolvedores e profissionais de controle de qualidade a lidar com problemas como erros do usuário e desempenho reduzido.

Há uma ampla variedade de níveis de registro para aplicativos e ações do usuário:

Para aplicações -> de “aviso” a “somente exceção”
Para ações do usuário -> de “registrar qualquer ação realizada por um usuário” a “registrar apenas ações destrutivas”

Os desenvolvedores podem optar por armazenar esses logs em qualquer lugar e em qualquer formato: arquivos de texto simples, bancos de dados, nuvem e serviços externos. Para fazer isso, podemos usar diferentes bibliotecas, incluindo NLog, Serilog e log4net.

As principais regras de implementação de registro de log  e criação de perfil são:

- Certifique-se de que o registro e a criação de perfil não afetem seu domínio.
- Esteja pronto para mudar suas abordagens de registro e criação de perfil a qualquer momento.


Como queríamos seguir a recomendação de adiar qualquer seleção de estrutura ao criar uma arquitetura limpa no .Net Core, usamos um logger integrado do .NET Core. A melhor maneira de usar esse logger no código é injetar a interface ILogger em cada classe e chamar o método LogInformation quando necessário.

No entanto tornamos implícito o registro de eventos do aplicativo e ações do usuário para que não precisemos chamar os métodos de registro de cada classe separadamente. E para fazer isso, criamos um comando personalizado e um pipeline de consulta.

A partir do MediatR 3.0, os desenvolvedores podem adicionar ações personalizadas (comportamentos) que serão executadas para qualquer solicitação. Para habilitar o log implícito, precisamos implementar a interface IPipelineBehavior<TRequest, TResponse> e registrá-la na classe Program. (Você pode encontrar instruções detalhadas para adicionar um pipeline personalizado ao seu projeto nesta documentação do MediatR.)

Essa técnica também pode ser usada para outros casos. Em nosso projeto, também o aplicamos para separar as operações de validação da lógica de negócios. Vamos ver como podemos melhorar nosso trabalho com validação.

Usando FluentValidation para validação de parâmetros

Depois de escrevermos nossas consultas e comandos, precisamos validar os parâmetros usados. A maneira mais fácil é escrever verificações diretas como if (parameter != null) {...}, mas fazer isso torna o código mais difícil de ler.

Uma opção um pouco melhor seria usar ferramentas como Guard ou Throw. Nesse caso, as chamadas de método seriam parecidas com Guard.NotNull(parameter) ou parameter.ThrowIfNull().

Seguindo os princípios da Clean Architecture qualquer coisa além da lógica de negócios deve ser conectada, então usamos a biblioteca FluentValidation. Essa biblioteca permite a implementação de validação de parâmetros personalizados, incluindo verificações assíncronas complexas.

Ao trabalhar com FluentValidation, devemos colocar todas as validações em um arquivo separado para cada comando ou consulta. Vejamos como isso funciona na prática.

Aqui usamos a classe PedidoQuery. Digamos que queremos encontrar um pedido usando o número de telefone de um cliente ou um número de pedido exclusivo. Para validar esses parâmetros, criamos uma classe PedidoQueryValidator separada:

public class PedidoQueryValidator : AbstractValidator<PedidoQuery>
{
  
public PedidoQueryValidator()
   {
     RuleFor(x => x.Telefone).NotEmpty().When(x => x.PedidoNumero <= 0);
     RuleFor(x => x.PedidoNumero).GreaterThan(0).NotNull().When(x => x.Telefone ==
null);
   }
}

Dessa forma, a validação é separada da lógica de negócios, o que melhora a legibilidade do código de nosso aplicativo da web.

Para resumir, ao adicionar um novo endpoint na API (se tudo estiver configurado) precisamos :

- Ter métodos de repositório para recuperar ou modificar dados
- Ter a funcionalidade necessária na lógica de negócios do aplicativo
- Criar um comando ou consulta
- Criar um manipulador digitado
- Criar um validador digitado
- Adicionar um controlador (se não existir)
- Adicionar um método ao controlador que enviará o comando ou consulta a um mediador
- Implementar o comportamento de log (se não existir)

Pode parecer chato criar muitos arquivos ao separar interesses dentro da arquitetura do seu aplicativo. Se você seguir os princípios descritos neste artigo, terá mais dois arquivos para cada método do controlador. Mas, dessa forma, seu código será bem estruturado, fácil de ler e compatível com o princípio de responsabilidade única.

O problema de criar arquivos pode ser facilmente resolvido escrevendo um script personalizado que crie modelos para as classes Command, CommandHandler e CommandValidator.

Assim, mostramos como você pode criar uma arquitetura limpa para uma aplicativo .Net e implementar os principais princípios do CQRS para facilitar a leitura, o teste e o aprimoramento do código do seu aplicativo. As etapas descritas acima nos permitiram separar a lógica de negócios de outras camadas de aplicativos — um requisito obrigatório da abordagem de Arquitetura Limpa.

Agora, nosso aplicativo de exemplo tem registro e validação conectados e o domínio é fácil de testar, pois a transparência do código nos permite pular algumas verificações triviais. Além disso, podemos criar qualquer comando ou consulta que precisarmos sem precisar alterar o domínio existente.

E estamos conversados...

"Portanto, nada julgueis antes de tempo, até que o Senhor venha, o qual também trará à luz as coisas ocultas das trevas, e manifestará os desígnios dos corações; e então cada um receberá de Deus o louvor."
1 Coríntios 4:5

Referências:


José Carlos Macoratti