CQRS: o princípio de "dividir e conquistar" a serviço de um programador

A arquitetura Puff é a salvação no mundo do desenvolvimento empresarial. Com sua ajuda, você pode descarregar o ferro, paralelizar processos e restaurar a ordem no código. Tentamos usar o padrão CQRS ao desenvolver um projeto corporativo. Tudo se tornou mais lógico e ... mais complicado. Recentemente, falei sobre o que eu tinha que enfrentar na reunião do Panda-Meetup C # .Net e agora estou compartilhando com você.



Você já reparou como é seu aplicativo corporativo? Por que não pode ser como Apple e Google? Sim, porque temos uma constante falta de tempo. Os requisitos mudam frequentemente, o prazo para as alterações é geralmente "ontem". E o que é mais desagradável, o negócio realmente não gosta de erros.



Para viver de alguma forma com isso, os desenvolvedores começaram a dividir seus aplicativos em partes. Tudo começou simples - com dados. Muitos estão familiarizados com o esquema quando os dados estão separados, o cliente está separado, enquanto a lógica é armazenada no mesmo local que os dados.



Bom esboço. Os maiores DBMSs possuem extensões procedurais totalmente funcionais para SQL. Existe um provérbio sobre a Oracle: "Onde há Oracle, há lógica." É difícil argumentar sobre a conveniência e a velocidade dessa configuração.

Mas temos um aplicativo corporativo, e há um problema: a lógica é difícil de escalar. E não é razoável carregar a capacidade do DBMS, que já sofre de problemas com a extração e atualização de dados, além de tarefas comerciais triviais.

Bem, para ser honesto, as ferramentas de programação da lógica de negócios incorporadas ao DBMS são fracas para a criação de aplicativos corporativos normais. Manter a lógica de negócios no T-SQL / PL-SQL é um problema. Não é à toa que as linguagens OOP são tão difundidas entre aplicativos corporativos: C #, Java, você não precisa ir muito longe por exemplo.



Parece uma solução lógica: destacamos a lógica de negócios. Ela viverá em seu servidor, a base por conta própria, o cliente por conta própria.

O que pode ser aprimorado nessa arquitetura de três camadas? A arquitetura está envolvida na camada da lógica de negócios, eu gostaria de evitar isso. A lógica de negócios não deseja saber nada sobre armazenamento de dados. A interface do usuário também é um mundo separado, no qual existem entidades que não são específicas da lógica de negócios.

Aumentar camadas ajudará. Esta solução parece quase perfeita, tem algum tipo de beleza interior.



Temos um DAL (Data Access Layer) - os dados são separados da lógica, geralmente um repositório CRUD usando ORM, além de procedimentos armazenados para consultas complexas. Esta opção permite que você desenvolva com rapidez suficiente e tenha um desempenho aceitável.

A lógica de negócios pode ser parte dos serviços ou ser uma camada separada. A interação entre as camadas pode ser realizada através de objetos de transporte (DTO).

A solicitação da interface do usuário vai para o serviço, ele se comunica com a lógica de negócios, entra no DAL para acessar os dados. Essa abordagem é chamada de camada N e possui vantagens claras.

Cada camada tem suas próprias metas e objetivos óbvios, dos quais nós, como programadores, gostamos muito. Cada camada de concreto está envolvida apenas em seus próprios negócios. Os serviços podem ser dimensionados horizontalmente. A abordagem é clara, mesmo para um desenvolvedor iniciante, uma pessoa entende rapidamente como o sistema funciona. É muito fácil rastrear todas as interações, pois a solicitação vai do início ao fim.

Outra consistência: todos os subsistemas do projeto funcionam com os mesmos dados, você não precisa se preocupar com o fato de termos gravado dados em um local e o usuário não os ver em outra parte.

Bolo de Camada 1. Nível N


Abaixo está um exemplo de um fragmento típico de um aplicativo criado com base nesses princípios. Temos uma exigência monetária, aqui examinei o modelo anêmico. E existe um repositório clássico, trabalho com o qual passa pelo ORM.



Este é um serviço típico, eles também são chamados de gerentes. Ele trabalha com o repositório, recebe solicitações e fornece respostas aos clientes. Neste serviço, vemos alguma confusão: temos um processo de processamento, um processo para trabalhar com a interface do usuário e um processo para algumas unidades de controle internas, elas são fracamente interconectadas.

Aqui está a aparência de um método típico desse serviço. Por exemplo, registro de uma reivindicação monetária.



Recebemos dados, realizamos algumas verificações comerciais. Depois, há uma atualização e, depois dela, algumas ações posteriores, por exemplo, enviando uma notificação ou gravação no log do usuário.

Nesta abordagem, apesar de toda a sua beleza, existem problemas. Muitas vezes, em aplicativos corporativos, a carga é assimétrica: as operações de leitura são uma ordem ou duas a mais que as gravações. Já existe um problema ao dimensionar o próprio banco de dados. Obviamente, isso é feito e, mesmo por meio de um DBMS em uma escala de banco de dados, o particionamento é chamado. Mas é difícil. Se isso for feito com as qualificações incorretas ou antes do necessário, o particionamento falhará.

Por exemplo, em um de nossos sistemas, o volume de dados atingiu 25 TB, surgiram problemas. Nós mesmos tentamos escalar, convidamos caras durões de uma empresa conhecida. Eles olharam e disseram: precisamos de 14 horas de tempo de inatividade completo da base. Nós pensamos e dissemos: pessoal, não vai dar certo, a empresa não vai aceitar.

Além do volume do banco de dados, o número de métodos em serviços e repositórios está aumentando. Por exemplo, em um serviço para reivindicações monetárias, existem mais de cem métodos. É difícil de manter, existem conflitos constantes quando a solicitação de mesclagem é mais difícil de executar. E se você levar em conta que os processos são diferentes, diferentes grupos de desenvolvedores trabalham neles, a tarefa de rastrear todas as alterações associadas a um problema se torna uma verdadeira dor de cabeça.

Massa folhada 2. CQRS


Então o que fazer? Existe uma solução que foi inventada na Roma antiga: dividir e governar.



Como se costuma dizer, tudo de novo é bem esquecido. Em 1988, Bertrand Meyer formulou o princípio da programação imperativa do CQS - separação de comandos e consultas - para trabalhar com objetos. Todos os métodos estão claramente divididos em dois tipos. A primeira consulta - consulta que retorna o resultado sem alterar o estado do objeto. Ou seja, quando você analisa os requisitos monetários do cliente, ninguém deve escrever no banco de dados que o cliente parecia tal e tal, não deve haver efeitos colaterais na solicitação.

O segundo - Comandos - comandos que alteram o estado de um objeto sem retornar dados. Ou seja, você pediu que algo mudasse e, em troca, não espere um relatório de 10 mil linhas.



Aqui, o modelo de dados de leitura é claramente separado do modelo de gravação. A maior parte da lógica de negócios funciona em operações de gravação. A leitura pode funcionar em representações materializadas ou em geral de maneira diferente. Eles podem ser divididos e sincronizados através de eventos ou alguns serviços internos. Existem muitas opções.

CQRS não é complicado. Devemos distinguir claramente as equipes que mudam o estado do sistema, mas não retornam nada. Aqui a abordagem pode ser mais equilibrada. Não é particularmente assustador se o comando retornar o resultado da execução: um erro ou, por exemplo, o identificador da entidade criada, não haverá crime nisso. É importante que a equipe não trabalhe com a solicitação, não procure dados e retorne entidades comerciais.

Pedidos - tudo é simples lá. Não altera a condição para que não haja efeitos colaterais. Isso significa que, se chamarmos a solicitação duas vezes seguidas e não houver outros comandos, o estado do objeto nos dois casos deverá permanecer idêntico. Isso permite paralelizar consultas. Curiosamente, um modelo separado para consultas não é necessário para o trabalho, porque não faz sentido desenhar lógica de negócios de um modelo de domínio para isso.

Nosso projeto CQRS


Aqui está o que queríamos fazer em nosso projeto:



Nosso aplicativo existente está em operação desde 2006, possui uma arquitetura clássica em camadas. Antiquado, mas ainda funcionando. Ninguém quer mudar isso e nem sabe com o que substituí-lo. Chegou o momento em que era necessário desenvolver algo novo, praticamente do zero. Em 2011-2012, o Event Sourcing e o CQRS foram um tópico muito na moda. Nós pensamos que era legal, para que pudéssemos armazenar o estado original do objeto e os eventos que levaram a ele.

Ou seja, não estamos atualizando o objeto. Há um estado original e próximo a ele é o que foi aplicado a ele. Nesse caso, há uma enorme vantagem - podemos restaurar o estado de um objeto a qualquer momento da história. De fato, a revista não é mais necessária. Ao armazenar eventos, entendemos o que exatamente aconteceu. Ou seja, não é apenas que o valor do cliente na célula "endereço" tenha sido atualizado, teremos o evento exato registrado, por exemplo, a mudança do cliente.

É claro que esse esquema funciona lentamente ao receber dados, portanto, temos um banco de dados separado com representações materiais para seleção. Bem, e sincronização de eventos: a cada chegada de eventos em uma alteração de estado, ocorre uma publicação. Em teoria, tudo parece estar bem, mas ... Eu nunca conheci pessoas que perceberam isso completamente na produção, em altas cargas com consistência comercial aceitável.



O esquema pode ser desenvolvido ainda mais se os manipuladores e comandos / solicitações forem separados. Aqui, como exemplo, temos uma equipe - uma reivindicação monetária registrada: há uma data, quantia, cliente e outros campos.

Nós colocamos uma restrição no processador de registro da reivindicação monetária de que ela só pode aceitar nossa equipe (onde TCommand: ICommand). Podemos escrever manipuladores sem alterar os antigos, simplesmente adicionando requisitos complexos. Por exemplo, primeiro atualize a data, depois anote o valor e aqui você envia uma notificação ao cliente - tudo isso é escrito em diferentes manipuladores por comando.

Como causamos tudo isso? Há um despachante que sabe onde ele tem todos esses manipuladores armazenados.



O expedidor é passado (por exemplo, por meio de um contêiner de DI) para a API. E quando o comando chega, ele apenas executa. Ele sabe onde está o contêiner, onde estão as equipes e os executa. Com pedidos - da mesma forma.

Qual é o problema desse esquema: todas as interações se tornam menos óbvias. Construímos uma hierarquia nos tipos registrados em contêineres e, em seguida, respondemos aos nossos comandos / solicitações. Requer um design muito claro da arquitetura. Qualquer ação com um método com um parâmetro não é mais limitada. Você escreve um comando, escreve um manipulador, registra-se em um contêiner. A quantidade de sobrecarga está aumentando. Em um projeto grande, surgem problemas com a navegação elementar. Decidimos seguir de uma maneira mais clássica.

Para comunicação assíncrona, foi utilizado o barramento de serviço Rebus.



Para tarefas simples, é mais do que suficiente.

O CQRS faz com que você aborde o código de maneira um pouco diferente, concentre-se no processo, porque todas as ações nascem como parte do processo. Alocamos um repositório para solicitações, comandos relacionados ao processamento feitos separadamente e processando solicitações relacionadas separadamente. Para leitura, não usamos um repositório separado, apenas trabalhamos com o ORM em equipes.



Aqui, por exemplo, é um método a partir do qual tudo supérfluo é jogado fora. Na equipe de registro de reivindicação de dinheiro, registramos a demanda e publicamos o evento no ônibus em que a reivindicação de dinheiro está registrada.



Qualquer pessoa interessada nisso reagirá. Por exemplo, a autenticação e o logon do usuário funcionarão lá.

Aqui está um exemplo de solicitação. Tudo ficou simples também: lemos e damos ao repositório.



Eu quero ficar separadamente no Rebus.Saga. Esse é um padrão que permite dividir uma transação comercial em ações atômicas. Isso permite bloquear não todos de uma vez, mas gradualmente e sucessivamente.



O primeiro elemento toma uma ação e envia uma mensagem, o segundo assinante responde, cumpre, envia sua mensagem, à qual a terceira parte do sistema já está respondendo. Se tudo terminar bem, o Saga gerará sua própria mensagem do tipo especificado, ao qual outros assinantes já responderão.

Vamos ver como é a classe para processar uma reivindicação de dinheiro neste caso. Tudo está claro: existem comandos, há solicitações relacionadas ao processo de registro, bem, um barramento com logs.



Nesse caso, há um manipulador. Quando um evento ocorre e uma equipe chega para registrar reivindicações monetárias, ele responde a ele. Por dentro, tudo é o mesmo de antes, mas a peculiaridade é que há um agrupamento por processo.



Por isso, ficou um pouco mais fácil, menos alterações em cada arquivo.

Conclusões


O que você precisa se lembrar ao trabalhar com o CQRS? Você precisa de uma abordagem melhor para o design, porque reescrever o processo é um pouco mais complicado. Há uma pequena sobrecarga, um pouco mais de aulas se tornaram, mas isso não é crítico. O código ficou menos conectado, no entanto, não é tanto por causa do CQRS, mas por causa da transição para o barramento. Mas foi o CQRS que nos levou a usar essa interação de evento. O código se tornou mais frequentemente adicionado do que alterado. Existem mais aulas, mas agora são mais especializadas.

Todo mundo precisa largar tudo e mudar massivamente para o CQRS? Não, você precisa analisar qual cenário funciona melhor para um projeto específico. Por exemplo, se o seu subsistema trabalha com diretórios, o CQRS não é necessário, a abordagem clássica em camadas fornece um resultado mais simples e conveniente.

A versão completa da performance no Panda Meetup está disponível abaixo.


Se você quiser se aprofundar no tópico, faz sentido estudar estes recursos:

Estilo de arquitetura CQRS - da Microsoft

Blog do Alexander Bendyu

Exemplos da Universidade Contoso com CQRS, MediatR, AutoMapper e mais - por Jimmy Bogard

CQRS - por Martin Fowler

Rebus

Source: https://habr.com/ru/post/pt440172/


All Articles