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
.Respostas:
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).
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:
isValid
mé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 validators
classes 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 validators
poderia 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.When a car uses Electric engine the only allowed transmission type is Automatic
deve ser verificada aqui.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
CommandDispatcher
usuá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.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 noHTTP 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:
fonte
EmailAddress
objeto de valor que se valide.EmailAddress
para 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.UserCanPlaceOrdersOnlyIfHeIsNotLockedValidator
. Você pode ver que esse é um domínio separado do dos Pedidos, portanto não pode ser validado pelo próprio OrderAggregate.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
ChangeEmail
comando, 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: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
User
agregado, 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 aUser
, mas realmente não. A "exclusividade" de um email se aplica a uma coleção deUsers
(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óriaUsers
) 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 umaUserEmailAlreadyExists
exceção). Como alternativa, um domínioUserService
pode ser responsabilizado por criar novosUsers
e 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.
fonte