Skip to content
This repository has been archived by the owner on Aug 25, 2021. It is now read-only.

Análise de implementação usando CQRS

Mateus Pereira edited this page Nov 8, 2020 · 15 revisions

O CQRS

CQRS, Command Query Responsability Segregation, é um padrão inicialmente descrito por Greg Young[1] e posteriormente por Martin Fowler[2]. Esse conceito, de forma rápida, envolve a criação de dois objetos que anteriormente eram apenas um. Em uma arquitetura baseada em MVC (Model, View, Controller), o Model seria o objeto mais próximo desse caso, mas não necessariamente precisa ser ele em um caso prático. A separação desse objeto se dá pelos métodos que são de leitura de dados (Query) e de mudança de estado (Command). Trazendo para a realidade de um banco de dados, por exemplo, uma query seria um comando SELECT, enquanto um command seria um INSERT, UPDATE ou DELETE.

Por mais simples que seja, tal padrão consegue reverter pensamentos acerca de como tratamos os dados e suas operações em um ambiente com comandos muitas vezes gerenciados pelo banco de dados. Com ele, devemos trabalhar com modelos de objetos únicos para ambas as interações, query e command. Consequentemente, ele é classificado por muitos como um design pattern, por questões de simplificação, enquanto é abordado como um conceito mais abrangente pelo próprio Greg Young. Tiremos então como exemplo o seguinte caso:

Um sistema de cadastro e login de usuários é controlado por uma tabela no banco de dados, tabela essa que possui o login, senha e id do usuário. Em circunstâncias normais, o processo de cadastro refere-se ao registro de uma nova linha nessa tabela, com um novo id, login e senha, enquanto o processo de login seria a leitura de uma tabela em que o login e senha solicitados estejam corretamente registrados no banco de dados.

Por um motivo qualquer, deseja-se utilizar agora dois bancos de dados para registrar os usuários cadastrados no sistema, ambos possuindo os mesmos usuários, respeitando as propriedades ACID e regras da tabela. Nessa situação, o processo de leitura deve conectar a um dos dois bancos de dados, enquanto a gravação deve registrar em ambos os bancos de dados. Assim, em uma implementação padrão, haveriam dois modelos, sendo um para cada banco de dados. Por isso, a gravação solicitaria os dois modelos, enquanto a leitura seria arbitrária. Agora, imagine tal modelo para n bancos de dados que seguissem as mesmas regras, com m tabelas diferentes. Tal implementação seria extremamente custosa e de baixa manutenibilidade.

Em um modelo seguindo o CQRS, primeiro devemos criar um novo modelo que defina seus atributos e manipuladores de operações. Logo em seguida, criamos duas interfaces, uma para os commands e outra para as queries. Por último, precisaremos implementar os modelos separados de cada banco de dados ou afins, modelos esses que manipulam em si os dados no banco. Desta forma, o modelo que interage diretamente com o código e a regra de negócio será o primeiro definido. Quando então for desejado efetuar uma operação que leia ou grave dados de um banco de dados, ele chamará os comandos pré-definidos dentro das implementações das interfaces de commands e queries. Só então esses quem manipularão os bancos de dados e escreverão as regras de leitura e gravação. Assim, o modelo principal utiliza os commands e queries como Facades, enquanto os commands e queries criam e salvam os modelos dos bancos de dados em uma estrutura de ponte, como o design pattern Bridge.

Implementações seguindo esse modelo do CQRS nos permite pensar que dois modelos que trabalham com os mesmos dados é possível e de forma organizada, o que nos dá a possibilidade de criar combinações de tratativas de dados além das limitações dos bancos de dados. Por exemplo, podemos dispor uma dezena de servidores com bancos de dados dedicados para a leitura de dados, enquanto apenas dois ou três para escrita e gravação de dados. Também podemos, desta forma, escrever regras de seleção de bancos de dados a serem usados dentro do modelo manipulador, abstraindo a informação para as camadas superiores, que continuariam funcionando exatamente da mesma forma como antes, chamando a operação dentro do modelo principal. Tudo isso permite que responsabilidades antes deixadas aos bancos de dados possam ser tomadas pelo código, prevenindo deadlocks nos bancos e mantendo a integridade de disponibilidade dos dados no sistema independentemente de onde eles estejam armazenados.

Uma das abordagens mais interessantes, levando em consideração a construção do modelo, é a utilização do CQRS dentro de um projeto elaborado com base no padrão DDD, Domain Driven Design. No padrão DDD, pode-se abordar os problemas de implementação em mais alto nível, permitindo isolar a implementação do CQRS exclusivamente nos repositórios enquanto os serviços continuam a funcionar, baseando os testes na consistência eventual do próprio modelo. Além do mais, tal padrão está fortemente relacionado a metodologias ágeis, com integrações contínuas e um alto nível de implementação. Portanto, discutir sobre CQRS não é interessante apenas pelos conceitos que ele define, mas pelas possibilidades que ele abre de abordar um mesmo problema por perspectivas diferentes.

Trabalhando em um caso prático

Um caso comum de implementação do CQRS seria em sistemas de vendas online. Tais sistemas possuem um conceito simples de venda de produtos com as mais diversas variações na base de dados, cada uma para abordar um problema específico da regra de negócio do projeto. Entretanto, em essência, esses sistemas possuem um grande tráfego de dados, registrando constantes transações e muitas vezes em cima de um mesmo produto, o que exige alta disponibilidade e fidelidade dos dados. Por isso, trabalhar nesse caso pode nos ser familiar e acabar facilitando na elucidação dos conceitos do padrão CQRS.

Trazendo para um caso ainda mais simples, vamos trabalhar com um sistema de pontos virtuais. Cada usuário do sistema possui seus próprios pontos, que podem ser usados em compras no formato de dinheiro virtual ou transferidos entre usuários. Se um usuário for transferir seus pontos para outro usuário, o procedimento será o seguinte:

  1. Usuário A lê pontos do usuário B.
  2. Novo número de pontos do usuário A = A - pontos_transferidos.
  3. Novo número de pontos do usuário B = B + pontos_transferidos.

Agora imagine que durante o processo de transferência de pontos do usuário A para o usuário B o usuário B também inicie uma transferência para o usuário A. Nessa situação, durante o processo de leitura dos pontos do usuário A na transferência de B para A, receberíamos o valor de A antes de descontar os pontos que ele vai transferir para o usuário B. No processo de transferência de pontos de A para B, o mesmo problema aconteceria. Assim, entramos em um caso de inconsistência dos dados, que poderia acontecer também, por exemplo, durante a compra de um mesmo produto por dois usuários diferentes e ao mesmo tempo, que será nosso principal ponto abordado.

Estrutura dos dados

Nosso principal modelo de dados será a venda. Quando um usuário desejar um produto, ele iniciará um processo de venda. Primeiro o usuário deverá informar se deseja utilizar seus pontos e, se desejado, quantos pontos deseja usar. Depois, se necessário o pagamento, uma transação efetuada por um sistema de terceiros ocorrerá. Com o correto pagamento, a compra é finalizada. Agora, se o pagamento não for efetuado na hora, a venda inicia um processo de cancelamento. O processo de cancelamento primeiro verifica se há pontos a serem estornados ao usuário e, se houverem, estorna tais pontos. Logo em seguida verifica-se se há um pagamento a ser estornado e, novamente, estorna se assim se mostrar necessário. Então, finalmente, a venda é registrada como cancelada. Tal processo de cancelamento também pode ser solicitado pelo próprio usuário, quando desejado.

Perceba que o modelo da venda está diretamente relacionado a três substantivos muito importantes: Um usuário comprador, a situação em que a venda se encontra e as maneiras que o usuário utilizou para debitar o valor total da compra. Dito isso, deixaremos claro que não declaramos o produto vendido nem sua disponibilidade em prol da diminuição da complexidade do escopo. Portanto, consideremos que não é necessário validar os valores das transações nem a disponibilidade do que estiver sendo vendido. Em uma definição de projeto mais robusta talvez tal definição seja um tanto quanto estranha, pois para que ocorra a venda, seria necessário vincula-la a um custo pré-definido, no mínimo, isso se não for essencial para o funcionamento. É nesse momento que entra a aplicação direta do conceito CQRS em um projeto modelado com base em domínios, e não modelos, tal qual o DDD faz.

Vejamos que o modelo da venda é um modelo existente no domínio do código, mas não necessariamente seria uma tabela no banco de dados. Esse modelo possuirá instâncias de outros três objetos: usuário, situação e pagamento. Mais especificamente, de acordo com o funcionamento do modelo, ele terá um usuário como comprador, a situação da venda e um ou mais pagamentos. Agora definiremos cada um dos outros três modelos.

Primeiro, o usuário, possuirá apenas um índice, um nome e um número de pontos disponíveis. O índice será único por usuário existente, enquanto o nome será apenas uma referência, podendo se repetir. Por último, os pontos serão valores inteiros, sendo esses utilizados nos pagamentos que utilizem os pontos do usuário. Portanto, um usuário nunca poderá ter pontos negativos, devendo ser sempre zero ou acima de zero.

A situação do pagamento controla as operações disponíveis pelo mesmo e, portanto, veremos o fluxo da venda como um autômato finito. A venda sempre será criada e sua situação será a inicial, podendo mudar de acordo com o processo de venda descrito anteriormente.

Agora o pagamento será responsável por confirmar ou não a venda, possuindo uma data de ocorrência e o usuário pagante. Nesse momento voltaremos a discussão quanto ao uso de modelos únicos no CQRS. Um pagamento pode ser efetuado usando pontos do usuário ou um gateway de terceiro. Entretanto, não importa qual o método, o modelo continua sendo de um pagamento, e a venda não se importa como ela ocorreu, e realmente não deve mesmo se importar. Para que isso ocorra, o modelo do pagamento deve conter a operação de pagamento, que será requisitada no processo da venda, que informe se o pagamento foi efetuado ou não, apenas.

Elaboração do escopo

Quando elaboramos um núcleo de sistema baseado no modelo dos dados, geralmente o fazemos pensando em operar os modelos, quase sempre transcrições fidedignas do banco de dados. Isso significa que ao solicitar uma compra, por exemplo, pensamos em pegar um usuário e registrar uma venda que possui todos os dados referentes a ela, como o estado em que se encontra, o valor, o método, usuário responsável e tudo mais que for desejado. Quando terminamos de informar tudo, jogamos no banco de dados. Se quisermos alguma informação, solicitamos a mesma tabela ao banco, buscando pelo identificador. Também quando desejamos atualizar alguma informação, atualizamos o banco de dados.

Por mais habituados que estejamos com tal maneira de modelar o problema, essa é engessada ao funcionamento do banco de dados e de maior complexidade para implementar sistemas mais robustos. É muito comum, nesses casos, aumentar o número de tabelas, relações entre elas e, posteriormente, a coleta dos dados se dá pelo grande número de uniões entre as tabelas, fazendo com que uma operação simples se torne difícil de entender mais adiante, fora a sobrecarga de requisições ao banco de dados.

Quando vamos criar um novo modelo que seja relacionado a outro, consequentemente precisaremos mudar o modelo anterior e todas as suas formas de atualização e requisição, para que nenhum dado seja perdido. Isso nos força a rever grande parte do código que podem facilmente quebrar. Acabamos tendo que lidar de duas formas: refatorando o banco e o sistema ou criando um novo modelo com uma nova regra independente. Refatorar o banco e o sistema, nesse modelo, se torna custoso até mesmo em pequenos projetos, enquanto criar um novo modelo é relativamente mais fácil. Entretanto, o processo de criação de uma nova regra acaba aumentando mais uma vez o escopo do projeto, o que também aumenta igualmente a dificuldade de refatora-lo futuramente. Isso mostra como essa metodologia de desenvolvimento pode gerar uma bola de neve na complexidade do escopo do projeto, tornando-o de difícil manutenção a longo prazo e, também, sobrecarregando o banco de dados.

Referências:

  1. Young Greg. “CQRS, Task Based UIs, Event Sourcing agh!”, on February 16, 2010. Site:http://codebetter.com/gregyoung/2010/02/16/cqrs-task-based-uis-event-sourcing-agh/
  2. Fowler Martin. “CQRS”, on July 14, 2011. Site:https://martinfowler.com/bliki/CQRS.html
  3. Yifan Zhong, Wei Li, and Jing Wang. 2019. Using Event Sourcing and CQRS to Build a High Performance Point Trading System. In Proceedings of the 2019 5th International Conference on E-Business and Applications (ICEBA 2019). Association for Computing Machinery, New York, NY, USA, 16–19. DOI:https://doi.org/10.1145/3317614.3317632
  4. M. Overeem, M. Spoor and S. Jansen, "The dark side of event sourcing: Managing data conversion," 2017 IEEE 24th International Conference on Software Analysis, Evolution and Reengineering (SANER), Klagenfurt, 2017, pp. 193-204, doi: 10.1109/SANER.2017.7884621.
  5. Cukier Daniel. "DDD - Introdução a Domain Driven Design", on July 16, 2010. Site:http://www.agileandart.com/2010/07/16/ddd-introducao-a-domain-driven-design/
  6. Gabriel Queiroz. "Vamos falar sobre Event Sourcing", on March 5, 2017. Site: https://medium.com/@gabrielqueiroz/vamos-falar-sobre-event-sourcing-276ae66106f7/