Como exatamente um comando CQRS deve ser validado e transformado em um objeto de domínio?

22

Estou adaptando o CQRS 1 do pobre homem há algum tempo, porque adoro a flexibilidade de ter dados granulares em um armazenamento de dados, oferecendo grandes possibilidades de análise e, assim, aumentando o valor comercial e, quando necessário, outro para leituras contendo dados desnormalizados para aumentar o desempenho .

Infelizmente, desde o início, tenho lutado com o problema em que exatamente devo colocar a lógica de negócios nesse tipo de arquitetura.

Pelo que entendi, um comando é um meio de comunicar intenção e não tem vínculos com um domínio por si só. Eles são basicamente objetos de transferência de dados (burros - se você desejar). Isso é para tornar comandos facilmente transferíveis entre diferentes tecnologias. O mesmo se aplica aos eventos que as respostas aos eventos concluídos com sucesso.

Em um aplicativo DDD típico, a lógica de negócios reside em entidades, objetos de valor, raízes agregadas, elas são ricas em dados e em comportamento. Mas um comando não é um objeto de domínio, portanto, não deve ser limitado a representações de dados de domínio, porque isso coloca muita pressão sobre eles.

Portanto, a verdadeira questão é: onde exatamente está a lógica?

Descobri que tendem a enfrentar essa luta com mais frequência ao tentar construir um agregado bastante complicado que define algumas regras sobre combinações de seus valores. Além disso, ao modelar objetos de domínio, gosto de seguir o paradigma à prova de falhas , sabendo que quando um objeto atinge um método, ele está em um estado válido.

Digamos que um agregado Caruse dois componentes:

  • Transmission,
  • Engine.

Ambos Transmissione Engineobjetos de valor são representados como tipos de super e ter acordo sub-tipos, Automatice Manualtransmissões, ou Petrole Electricmotores, respectivamente.

Nesse domínio, viver sozinho um criado com sucesso Transmission, seja ele Automaticou Manualou qualquer um dos tipos Engineé completamente bom. Mas o Caragregado apresenta algumas novas regras, aplicáveis ​​apenas quando Transmissione Engineobjetos são usados ​​no mesmo contexto. Nomeadamente:

  • Quando um carro usa o Electricmotor, o único tipo de transmissão permitido é Automatic.
  • Quando um carro usa o Petrolmotor, ele pode ter um ou outro tipo de Transmission.

Eu pude detectar essa violação da combinação de componentes no nível da criação de um comando, mas, como afirmei anteriormente, pelo que entendi isso não deve ser feito, porque o comando conteria lógica de negócios que deveria ser limitada à camada de domínio.

Uma das opções é mover essa validação da lógica de negócios para o próprio comando validador, mas isso também não parece certo. Parece que eu estaria desconstruindo o comando, verificando suas propriedades recuperadas usando getters e comparando-as no validador e inspecionando os resultados. Isso grita como uma violação da lei de Demeter para mim.

Descartando a opção de validação mencionada porque ela não parece viável, parece que se deve usar o comando e construir o agregado a partir dele. Mas onde deveria existir essa lógica? Deveria estar dentro do manipulador de comando responsável por manipular um comando concreto? Ou deveria estar dentro do validador de comando (também não gosto dessa abordagem)?

Atualmente, estou usando um comando e crio um agregado a partir dele no manipulador de comando responsável. Mas quando faço isso, se eu tiver um validador de comando, ele não conterá nada, porque, se o CreateCarcomando existir, ele conterá componentes que eu sei que são válidos em casos separados, mas o agregado pode dizer diferente.


Vamos imaginar um cenário diferente misturando diferentes processos de validação - criando um novo usuário usando um CreateUsercomando.

O comando contém um Iddos usuários que serão criados e os seus Email.

O sistema declara as seguintes regras para o endereço de email do usuário:

  • deve ser único,
  • não deve estar vazio,
  • deve ter no máximo 100 caracteres (comprimento máximo de uma coluna db).

Nesse caso, mesmo que ter um email exclusivo seja uma regra de negócios, fazer check-lo de forma agregada faz muito pouco sentido, porque eu precisaria carregar todo o conjunto de emails atuais no sistema em uma memória e verificar o email no comando contra o agregado ( Eeeek! Algo, algo, desempenho.). Por esse motivo, eu moveria essa verificação para o validador de comando, que seria UserRepositoryuma dependência e usaria o repositório para verificar se já existe um usuário com o email presente no comando.

Quando se trata disso, de repente faz sentido colocar as outras duas regras de email no validador de comando. Mas tenho a sensação de que as regras devem estar realmente presentes em um Useragregado e que o validador de comando deve verificar apenas a exclusividade e, se a validação for bem-sucedida, devo criar o Useragregado no CreateUserCommandHandlere transmiti-lo para um repositório para ser salvo.

Sinto-me assim porque é provável que o método save do repositório aceite um agregado, o que garante que, assim que o agregado for aprovado, todos os invariantes sejam atendidos. Quando a lógica (por exemplo, a não-vazio) só está presente dentro da validação comando em si um outro programador pode ignorar completamente este validação e chamar o método Save na UserRepositorycom um Userobjeto diretamente o que poderia levar a um erro de banco de dados fatal, porque o e-mail pode ter faz muito tempo.

Como você lida pessoalmente com essas validações e transformações complexas? Estou mais feliz com a minha solução, mas sinto que preciso afirmar que minhas idéias e abordagens não são completamente estúpidas para ficar muito feliz com as escolhas. Estou totalmente aberto a abordagens completamente diferentes. Se você tem algo que tentou e trabalhou muito bem para você, adoraria ver sua solução.


1 Trabalhando como desenvolvedor PHP responsável por criar sistemas RESTful, minha interpretação do CQRS se desvia um pouco da abordagem padrão de processamento de comandos assíncronos , como às vezes retornando resultados de comandos devido à necessidade de processar comandos de forma síncrona.

Andy
fonte
preciso de algum código de exemplo, eu acho. como são seus objetos de comando e onde você os cria?
Ewan
@ Ewan Vou adicionar exemplos de código hoje ou amanhã. Partindo para uma viagem em alguns minutos.
413 Andy
Ser um programador de PHP eu sugiro dar uma olhada na minha implementação CQRS + ES: github.com/xprt64/cqrs-es
Constantin Galbenu
@ConstantinGALBENU Se considerarmos a interpretação de Greg Young do CQRS correta (o que provavelmente deveríamos), seu entendimento do CQRS está errado - ou pelo menos sua implementação do PHP. Os comandos não devem ser manipulados por agregados diretamente. Os comandos devem ser manipulados por manipuladores de comando que podem produzir alterações em agregados que produzem eventos a serem usados ​​para replicações de estado.
513 Andy
Não acho que nossas interpretações sejam diferentes. Você só precisa se aprofundar mais no DDD (no nível tático dos Agregados) ou abrir os olhos mais. Existem pelo menos dois estilos de implementação do CQRS. Eu uso um deles. Minha implementação se assemelha mais ao modelo do ator e torna a camada Application muito fina, o que é sempre uma coisa boa. Observei que há muita duplicação de código nesses serviços de aplicativo e decidi substituí-los por um CommandDispatcher.
Constantin Galbenu

Respostas:

22

A resposta a seguir está no contexto do estilo CQRS promovido pelo cqrs.nu no qual os comandos chegam diretamente nos agregados. Nesse estilo arquitetural, os serviços de aplicativo estão sendo substituídos por um componente de infraestrutura (o CommandDispatcher ) que identifica o agregado, carrega, envia o comando e depois persiste o agregado (como uma série de eventos se a Origem de Eventos for usada).

Portanto, a verdadeira questão é: onde exatamente está a lógica?

Existem vários tipos de lógica (validação). A idéia geral é executar a lógica o mais cedo possível - falhe rapidamente, se desejar. Portanto, as situações são as seguintes:

  • a estrutura do próprio objeto de comando; o construtor do comando possui alguns campos obrigatórios que devem estar presentes para que o comando seja criado; esta é a primeira e mais rápida validação; isso está obviamente contido no comando.
  • validação de campo de baixo nível, como o não vazio de alguns campos (como o nome de usuário) ou o formato (um endereço de email válido). Esse tipo de validação deve estar contido no próprio comando, no construtor. Existe outro estilo de ter um isValidmétodo, mas isso me parece inútil, pois alguém teria que se lembrar de chamá-lo quando, na verdade, uma instanciação de comando bem-sucedida for suficiente.
  • command validatorsclasses separadas que têm a responsabilidade de validar um comando. Uso esse tipo de validação quando preciso verificar informações de vários agregados ou fontes externas. Você pode usar isso para verificar a exclusividade de um nome de usuário. Command validatorspoderia ter quaisquer dependências injetadas, como repositórios. Lembre-se de que essa validação é eventualmente consistente com o agregado (por exemplo, quando o usuário é criado, outro usuário com o mesmo nome de usuário pode ser criado nesse meio tempo)! Além disso, não tente colocar aqui a lógica que deve residir dentro do agregado! Os validadores de comando são diferentes dos gerenciadores do Sagas / Process, que geram comandos com base em eventos.
  • os métodos agregados que recebem e processam os comandos. Esta é a última (tipo de) validação que ocorre. O agregado extrai os dados do comando e usa alguma lógica de negócios principal que aceita (executa alterações no seu estado) ou os rejeita. Essa lógica é verificada de uma maneira consistente e forte. Esta é a última linha de defesa. No seu exemplo, a regra When a car uses Electric engine the only allowed transmission type is Automaticdeve ser verificada aqui.

Sinto-me assim porque o método save do repositório provavelmente aceita um agregado, o que garante que, uma vez que o agregado seja passado, todos os invariantes sejam atendidos. Quando a lógica (por exemplo, o não vazio) está presente apenas na validação de comando, outro programador pode ignorar completamente essa validação e chamar o método save no UserRepository com um objeto User diretamente, o que pode levar a um erro fatal no banco de dados, porque o email pode ter demorado muito.

Usando as técnicas acima, ninguém pode criar comandos inválidos ou ignorar a lógica dentro dos agregados. Os validadores de comando são carregados automaticamente + chamados pelo CommandDispatcherusuário para que ninguém possa enviar um comando diretamente ao agregado. Pode-se chamar um método no agregado que passa um comando, mas não pode persistir nas alterações, portanto, seria inútil / inofensivo fazê-lo.

Trabalhando como desenvolvedor PHP responsável por criar sistemas RESTful, minha interpretação do CQRS se desvia um pouco da abordagem padrão de processamento de comandos assíncronos, como às vezes retornando resultados de comandos devido à necessidade de processar comandos de forma síncrona.

Também sou programador PHP e não retorno nada dos meus manipuladores de comando (métodos agregados no formulário handleSomeCommand). No entanto, muitas vezes, retorno informações ao cliente / navegador no HTTP response, por exemplo, o ID da raiz agregada recém-criada ou algo de um modelo de leitura, mas nunca retorno (realmente nunca ) nada dos meus métodos de comando agregados. O simples fato de o comando ter sido aceito (e processado - estamos falando sobre processamento síncrono do PHP, certo ?!) é suficiente.

Retornamos algo ao navegador (e ainda fazemos CQRS pelo livro) porque o CQRS não é uma arquitetura de alto nível .

Um exemplo de como os validadores de comando funcionam:

Caminho do comando por meio de validadores de comando a caminho do Agregado

Constantin Galbenu
fonte
No que diz respeito à sua estratégia de validação, o ponto número dois salta para mim como um local provável em que a lógica será duplicada frequentemente. Certamente alguém gostaria que o Usuário agregasse validar um email não vazio e bem formado, assim como não? Isso se torna aparente quando introduzimos um comando ChangeEmail.
king-side-slide
@ king-side-slide não se você tiver um EmailAddressobjeto de valor que se valide.
Constantin Galbenu
Isso é inteiramente correto. Pode-se encapsular um EmailAddresspara reduzir a duplicação. Mais importante, porém, ao fazer isso, você também moveria a lógica do seu comando para o seu domínio. Vale a pena notar que isso pode levar muito longe. Muitas vezes, conhecimentos semelhantes (objetos de valor) podem ter requisitos de validação diferentes, dependendo de quem os utiliza. EmailAddressé um exemplo conveniente porque toda a concepção desse valor possui requisitos de validação global.
king-side-slide
Da mesma forma, a idéia de um "validador de comando" parece desnecessária. O objetivo não é impedir que comandos inválidos sejam criados e despachados. O objetivo é impedi-los de executar. Por exemplo, posso transmitir qualquer dado que desejar com um URL. Se for inválido, o sistema rejeita minha solicitação. O comando ainda é criado e despachado. Se um comando exigir várias agregações para validação (ou seja, uma coleção de Usuários para verificar a exclusividade do email), um serviço de domínio é mais adequado. Objetos como "x validator" geralmente são um sinal de um modelo anêmico no qual os dados estão sendo separados do comportamento.
king-side-slide
1
@ king-side-slide Um exemplo concreto é UserCanPlaceOrdersOnlyIfHeIsNotLockedValidator. Você pode ver que esse é um domínio separado do dos Pedidos, portanto não pode ser validado pelo próprio OrderAggregate.
Constantin Galbenu
6

Uma premissa fundamental do DDD é que os modelos de domínio se validam. Esse é um conceito crítico, pois eleva seu domínio como a parte responsável por garantir que suas regras de negócios sejam aplicadas. Ele também mantém seu modelo de domínio como foco do desenvolvimento.

Um sistema CQRS (como você indica corretamente) é um detalhe de implementação que representa um subdomínio genérico que implementa seu próprio mecanismo coeso. Seu modelo não deve depender de nenhuma parte da infraestrutura do CQRS para se comportar de acordo com as regras de negócios. O objetivo do DDD é modelar o comportamento de um sistema para que o resultado seja uma abstração útil dos requisitos funcionais do seu domínio de negócios principal. Mover qualquer parte desse comportamento para fora do seu modelo, por mais tentador que seja, reduz a integridade e a coesão do seu modelo (e o torna menos útil).

Simplesmente estendendo seu exemplo para incluir um ChangeEmailcomando, podemos ilustrar perfeitamente por que você não deseja nenhuma lógica de negócios em sua infraestrutura de comandos, pois seria necessário duplicar suas regras:

  • o email não pode estar vazio
  • o email não pode ter mais de 100 caracteres
  • o email deve ser único

Portanto, agora que podemos ter certeza de que nossa lógica precisa estar em nosso domínio, vamos abordar a questão de "onde". As duas primeiras regras podem ser facilmente aplicadas ao nosso Useragregado, mas essa última regra é um pouco mais sutil; um que exige mais conhecimento para obter uma percepção mais profunda. Na superfície, pode parecer que essa regra se aplica a a User, mas realmente não. A "exclusividade" de um email se aplica a uma coleção de Users(de acordo com algum escopo).

Ah ha! Com isso em mente, fica muito claro que sua UserRepository(sua coleção de memórias em memória Users) pode ser uma melhor candidata para impor esse invariante. O método "save" é provavelmente o local mais razoável para incluir a verificação (onde você pode lançar uma UserEmailAlreadyExistsexceção). Como alternativa, um domínio UserServicepode ser responsabilizado por criar novos Userse atualizar seus atributos.

Falhar rápido é uma boa abordagem, mas só pode ser feita onde e quando se encaixa no restante do modelo. Pode ser extremamente tentador verificar os parâmetros em um método (ou comando) de serviço de aplicativo antes de processar mais uma tentativa de detectar falhas quando você (o desenvolvedor) sabe que a chamada falhará em algum lugar mais profundo do processo. Mas, ao fazer isso, você duplicará (e vazou) o conhecimento de uma maneira que provavelmente exigirá mais de uma atualização no código quando as regras de negócios mudarem.

deslize do lado do rei
fonte
2
Eu concordo com isto. Minha leitura até agora (sem o CQRS) me diz que a validação sempre deve ser feita no modelo de domínio para proteger os invariantes. Agora, estou lendo o CQRS e está me dizendo para colocar a validação nos objetos de comando. Isso parece contra-intuitivo. Você conhece algum exemplo, por exemplo, no GitHub, onde a validação é colocada no Modelo de Domínio, em vez do Comando? +1.
w0051977 5/06