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:
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:
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:
NET - Unit of Work - Padrão Unidade de ...