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 Car
use dois componentes:
Transmission
,Engine
.
Ambos Transmission
e Engine
objetos de valor são representados como tipos de super e ter acordo sub-tipos, Automatic
e Manual
transmissões, ou Petrol
e Electric
motores, respectivamente.
Nesse domínio, viver sozinho um criado com sucesso Transmission
, seja ele Automatic
ou Manual
ou qualquer um dos tipos Engine
é completamente bom. Mas o Car
agregado apresenta algumas novas regras, aplicáveis apenas quando Transmission
e Engine
objetos são usados no mesmo contexto. Nomeadamente:
- Quando um carro usa o
Electric
motor, o único tipo de transmissão permitido éAutomatic
. - Quando um carro usa o
Petrol
motor, ele pode ter um ou outro tipo deTransmission
.
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 CreateCar
comando 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 CreateUser
comando.
O comando contém um Id
dos 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 UserRepository
uma 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 User
agregado e que o validador de comando deve verificar apenas a exclusividade e, se a validação for bem-sucedida, devo criar o User
agregado no CreateUserCommandHandler
e 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 UserRepository
com um User
objeto 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.
CommandDispatcher
.