Design: CQRS - Desfazendo mal-entendidos
William Santos
Posted on August 19, 2022
Olá!
Este é mais um post da seção Design e, desta vez, trago o conteúdo em um formato diferente. Não pretendo explicar detalhadamente o pattern arquitetural CQRS, mas sim esclarecer alguns pontos sobre ele que são comumente confundidos -- e muito difundidos! -- pela comunidade .NET.
Vou começar com duas provocações:
- CQRS não tem a ver com MediatR;
- CQRS, sem um domínio complexo, não é CQRS!
Perdido? Já esclareço. Vamos lá!
O objetivo do pattern
Quando Greg Young propôs o pattern, em 2010, o problema que ele visava resolver era a impossibilidade de conciliar um modelo de domínio rico, mais especificamente um Agregado do DDD, com um padrão arquitetural baseado em dados, que suporta apenas 4 verbos (Create, Read, Update e Delete, o famoso CRUD).
Quando utilizamos DDD, nossa principal preocupação é ter claro o jargão do domínio, aquilo que Eric Evans vai chamar de "Linguagem Ubíqua (ou Onipresente)". Ou seja, se um dado modelo de domínio possui um verbo (que se traduzirá em um método) chamado, por exemplo, "Cancelar", simplesmente não haverá um método correspondente para este verbo em uma arquitetura baseada em CRUD (repare que, no caso de uma compra, por exemplo, "Cancelar" não é o mesmo que "Deletar/Excluir").
Além disso, existem duas questões quanto à interface com o usuário: 1) interfaces do tipo CRUD se preocupam, exclusivamente, com o estado do DTO que a alimenta, enquanto em DDD estamos mais preocupados com o comportamento do modelo de domínio. Isso demanda um estilo de desenvolvimento de interface conhecido como "Interface baseada em tarefas". Ou seja, a ação a ser executada é mais importante que o estado do modelo e; 2)nem sempre essa interface demanda todos os dados constantes do modelo de domínio, o que acaba gerando a necessidade de criar mapeamentos entre seu modelo de domínio e o DTO enviado ao usuário o que, além de dar mais trabalho, acopla seu domínio à sua camada de aplicação (seu controller, caso de uso ou qualquer que seja sua representação).
Com isso em mente, Greg Young resolveu pensar numa forma de criar um pattern arquitetural que desse conta dos problemas apresentados.
A origem do pattern
Greg Young se baseou em uma ideia proposta por Bertrand Meyer que, basicamente, dizia o seguinte:
"Uma operação que altera o estado de um objeto não deve retornar valor. E uma operação que retorne um valor não deve alterar o estado de um objeto".
Esta proposta ficou conhecida como CQS, que significa, em português, "Segregação entre Consulta e Comando".
Ao contrário da ideia do CQS, no entanto, que eventualmente precisa ser violada (como no método pop de um array, que altera seu estado removendo um elemento mas também o retorna), o CQRS se pretende manter puro. Ou seja, um comando não deve retornar um valor (embora possa retornar um resultado), e uma consulta não deve executar um comando.
O aspecto mais importante, no entanto, não é a mera separação entre comandos e consultas mas, sim, a separação entre modelo de domínio e modelo de apresentação (DTO). E é neste ponto que começo a responder às provocações acima.
CQRS não tem nada a ver com MediatR
Muita gente acredita, por repetir informações incorretas ou de fontes questionáveis, que basta fazer uma implementação com MediatR, criando CommandHandlers e QueryHandlers e, pronto!, temos CQRS.
Pois bem. Esta crença está, completamente, incorreta!
Lembre-se do que foi dito acima: mais importante que separar comandos de consultas, é separar o modelo de domínio do modelo de apresentação. Portanto, se você apenas faz uma implementação com MediatR, mas segue utilizando DTOs que representam o estado do seu modelo de domínio (que, geralmente, vão refletir seu modelo de persistência), você estará, apenas, adicionando código desnecessário e, potencialmente, criando redundâncias como, por exemplo, repetir em sua camada de persisência a estrutura do DTO utilizado pela camada de aplicação. Você poderá dizer que está utilizando o Mediator Pattern, mas não que está implementando CQRS!
Nota: não tenho a pretensão de me colocar como uma fonte confiável. Estou, apenas, reproduzindo o conteúdo do próprio autor do pattern e, inclusive, estou sujeito a erros de interpretação. Portanto, eventuais críticas que me realinhem a este conteúdo serão muito bem-vindas!
Além disso, há um segundo detalhe: MediatR sequer é necessário à implementação de CQRS. Não há, necessariamente, problema algum em sua camada de aplicação (por exemplo, um Controller em sua Web API) ter conhecimento sobre o CommandHandler ou QueryHandler -- isso sem dizer que haverá um overhead no uso do MediatR, uma vez que o mesmo exige a criação de uma nova instância do mediador a cada ativação.
Nota: É claro que, se você entender que este trade-off entre desempenho e desacoplamento tem uma relação positiva, não há razão para não usar, inclusive tenho um tutorial sobre o MediatR publicado no blog. Aliás, faça um teste! Crie uma Web API com e uma sem MediatR e faça um teste de performance. É um exercício bem divertido!
Aí você poderia me perguntar: mas, neste caso, vou injetar um monte de handlers no meu Controller?
E a resposta é: definitavamente, não!
Pessoalmente, não recomendo o uso de um mesmo Controller para executar mais de uma operação neste caso, assumindo que seu Controller seja o equivalente ao seu Caso de Uso. A alternativa é organizar seus Controllers não a partir do modelo de domínio que ele afeta (por ex.: OrderController), mas sim a partir das ações do usuário -- lembra da ideia de interface baseada em tarefas? É aqui que ela faz sentido na camada de aplicação. Ou seja, seguindo o exemplo acima, teríamos um PlaceOrderController, que teria injetado um PlaceOrderCommandHandler (ou uma instância de IMediatR) e faria o restante do trabalho.
CQRS, sem um domínio complexo, não é CQRS
Acredito que, a esta altura, este ponto esteja claro. Mas quero adicionar alguns comentários.
Existem algumas estratégias possíveis para separar seu modelo de domínio do modelo de apresentação, então vou citá-las para conhecimento.
Eventos e Projeções
Esta é a primeira forma recomendada pelo Greg Young. Quando um comando é executado, e o estado do modelo de domínio é alterado, um evento de domínio é lançado e, a partir dele, o modelo de apresentação é atualizado. Neste cenário, existirão duas áreas de persistência: uma para o modelo de domínio, e outra para o modelo de apresentação.
Note que, neste caso, a consistência será, necessariamente, eventual, já que haverá algum tempo decorrido entre o lançamento do evento, sua captura, processamento, e atualização do modelo de apresentação.
Nota: uma questão muito importante nesta situação é a garantia de ordem no processamento dos eventos. Caso os eventos sejam processados fora de ordem, o estado tanto de seu modelo de domínio, quanto de seu modelo de apresentação, serão corrompidos. Este é um ponto de atenção ao se optar por este modelo de atualização.
Eventos como Persistência
Esta é a segunda forma recomendada pelo Greg Young. Da mesma forma que acima, um evento de domínio é lançado após a execução de um comando mas, em vez de atualizar o estado do modelo de domínio, o próprio evento seria armazeado, e o estado do modelo seria um reflexo do processamento desses eventos em ordem de ocorrência -- e aí temos um outro pattern, chamado Event Sourcing.
Conclusão
Como pudemos ver acima, CQRS é muito mais que apenas separar algumas porções de código e incluir o MediatR como uma espécie de service locator. Se você não possui um domínio complexo, que demande essa diferenciação entre modelos de domínio e modelos de apresentação, é interessante abrir mão desta abordagem para manter seu código simples.
Do contrário, vale muito a pena considerar o uso de CQRS junto com Event Sourcing, uma vez que estes patterns são complementares.
Gostou? Me deixe saber pelos indicadores.
Tem dúvidas, críticas ou sugestões? Deixe um comentário ou me procure nas redes sociais.
Até a próxima!
Posted on August 19, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.