Origem do evento, um evento, estado de dois agregados alterados
Estou tentando aprender maneiras de DDD e assuntos relacionados. Eu tive uma idéia de um contexto limitado simples para implementar o "banco": existem contas, o dinheiro pode ser depositado, sacado e transferido entre eles. Também é importante manter o histórico de mudanças.
Identifiquei a entidade da conta e que a fonte do evento seria boa para acompanhar as alterações nela. Outras entidades ou objetos de valor são irrelevantes para o problema, portanto, não os mencionarei.
Ao considerar depósitos e saques - é relativamente simples, porque há apenas um agregado modificado.
Ao transferir é diferente - duas agregações devem ser modificadas por um evento MoneyTransferred . O DDD descontinua a modificação de vários agregados em uma transação. Por outro lado, a regra da fonte de eventos é aplicar eventos às entidades e modificar o estado com base nelas. Se o evento pudesse ser armazenado simplesmente no banco de dados, não haveria problema. Mas, para impedir a modificação simultânea de entidades originadas por eventos, devemos implementar alguma versão do fluxo de eventos de cada agregado (para manter seus limites de transação). Com o controle de versão, surge outro problema - não posso usar estruturas simples para armazenar eventos e lê-los novamente para aplicá-los à agregação.
Minha pergunta é: como posso reunir esses três princípios: "uma transação agregada", "evento-> alteração no agregado" e "prevenção de modificação simultânea"?
— cocsackie
Ao transferir é diferente - duas agregações devem ser modificadas por um evento MoneyTransferred.
Transferir dinheiro é um ato separado da atualização dos livros contábeis.
O exercício que finalmente revelou isso para mim foi perceber que AccountOverdrawné um evento; ele descreve o estado da conta sem levar em consideração os outros participantes dessa troca; portanto, deve haver um comando executado em uma conta que a produz.
Você não pode derivar razoavelmente estados como AccountOverdrawnno modelo de leitura, porque você não pode saber se já viu todos os eventos ainda - apenas o agregado em si tem uma visão completa da história a qualquer momento.
A resposta, é claro, está lá no idioma onipresente - as contas são creditadas ou debitadas para refletir as obrigações do banco para com seus clientes.
Tudo bem, mas isso significa que eu devo usar os eventos AccountCredited e AccountDebited para depósitos e saques também, portanto, apenas registro não a causa da mudança, mas a mudança causada por alguma outra ação. Se eu gostaria de reverter a ação, não poderia, porque nem todos os eventos são registrados.
Não sei ao certo o que se segue, porque você possui (para casos como este) um identificador de correlação natural, que é o próprio ID da transação.
Segunda coisa - significa que eu preciso usar algo como saga.
Ortografia ligeiramente diferente: você precisa de algo como um ser humano enviando os comandos certos .
Há pelo menos duas maneiras de fazer isso. Um seria ter um assinante ouvindo MoneyTransferrede despachando os dois comandos para os livros.
Outra alternativa seria rastrear o processamento da transação como um agregado separado - pense nela como uma lista de verificação de todas as coisas que precisam ser feitas desde que uma transação ocorreu. Portanto, um MoneyTransferredmanipulador de eventos despacha ProcessTransaction, que agenda o trabalho a ser feito e verifica qual trabalho foi concluído.
Tudo bem, mas isso significa que eu devo usar os eventos AccountCredited e AccountDebited para depósitos e saques também, portanto, apenas registro não a causa da mudança, mas a mudança causada por alguma outra ação. Se eu gostaria de reverter a ação, não poderia, porque nem todos os eventos são registrados. Como posso fazer isso (causalidade de eventos)? Segunda coisa - significa que eu preciso usar algo como saga. Como deve ser modelada uma transferência então? No momento em que tenho o método de transferência por conta. Quando chamado, publica o evento MoneyTransferred . Não sei o que deve começar algo como saga.
Não é -> AccountCredited e AccoundDebited e MoneyTransferred ? A primeira solução atualiza os dois agregados em uma transação (nenhuma garantia de consistência de qualquer tipo)? Também não há agregado que possa publicar MoneyTransferred -> sem correlação. A segunda solução parece ser melhor - ProcessTransaction pode publicar MoneyTransferred e, para evitar várias modificações agregadas em uma transação, posso publicar eventos da Conta após confirmar a transação. Desculpe por ser meticuloso. É difícil de entender para iniciantes - não pode usar apenas um padrão sem outro.
Um detalhe importante no entendimento de contas baseadas em transações: o balanceatributo de accounté na verdade uma instância de desnormalização. Está lá por conveniência. Na realidade, o saldo de uma conta é a soma de suas transações, e você realmente não precisa que a própria conta tenha um saldo.
Tendo isso em mente, o ato de transferir um dinheiro não deve ser atualizar, accountmas inserir transaction.
Dito isto, há outra regra importante: o ato de adicionar a transactiondeve ser atômico com uma atualização do (campo de equilíbrio desnormalizado de) account.
Agora, se eu entendo o conceito DDD de agregados, o seguinte parece relevante:
O agregado é um limite lógico para coisas que podem mudar em uma transação comercial de um determinado contexto. Um agregado pode ser representado por uma única classe ou por várias classes. Se mais de uma classe constitui um agregado, uma delas é a chamada classe ou entidade raiz. Todo o acesso ao agregado de fora deve acontecer através da classe raiz.
Então, em termos de design DDD, eu sugeriria:
Há um agregado para representar a transferência
O agregado é composto pelos seguintes objetos: a transferência (o objeto raiz); o objeto raiz está vinculado a duas listas de transações (uma para cada conta); e cada lista de transações está vinculada a uma conta.
Todo o acesso à transferência deve ser meditado pelo objeto raiz (the transfer).
Se você estiver tentando implementar o suporte à transferência assíncrona, seu código principal deve se preocupar apenas em criar a transferência, no status "pendente". Você pode ter outro segmento ou um trabalho que realmente mova o dinheiro (inserindo no histórico de transações e, portanto, atualizando saldos) e defina a transferência como "lançada".
Se você deseja implementar uma transação de transferência de bloqueio em tempo real, a lógica de negócios deve criar uma transfere esse objeto coordenaria as outras atividades em tempo real.
Em termos de prevenção de problemas de simultaneidade, a primeira ordem do dia deve ser inserir a transação de débito na lista de transações da conta de origem (atualizar o saldo, é claro). Isso teria que ser executado atomicamente no nível do banco de dados (através de um procedimento armazenado). Após a ocorrência do débito, o restante da transferência deverá ser bem-sucedido, independentemente de problemas de simultaneidade, pois não deve haver nenhuma regra comercial que impeça um crédito na conta de destino.
(No mundo real, as contas bancárias têm o conceito de uma postagem de memorando que suporta o conceito de confirmação lenta de duas fases. A criação da postagem de memorando é leve e fácil, e também pode ser revertida sem problemas. A mensagem de memorando para uma postagem difícil é quando o dinheiro realmente se move - isso não pode ser revertido - e representa a segunda fase do commit de duas fases, ocorrendo somente após todas as regras de validação terem sido verificadas).
Atualmente, também estou na fase de aprendizado. Do ponto de vista da implementação, é assim que sinto que você executará essa ação.
Dispatch TransferMoneyCommand que gera os seguintes eventos [MoneyTransferEvent, AccountDebitedEvent]
Observe que antes de gerar esses eventos, será necessário executar a validação superficial do comando e a lógica do domínio, ou seja, a conta possui saldo suficiente?
Persista os eventos (com controle de versão) para garantir que não haja problemas de consistência. Observe que pode haver outro comando simultâneo (como retirar todo o dinheiro) que conseguiu obter êxito e salvar eventos anteriores a esse, portanto o estado atual do agregado pode estar desatualizado e, portanto, os eventos são gerados no estado antigo e estão incorretos. Se o salvamento dos eventos falhar, você precisará tentar novamente o comando desde o início.
Depois que os eventos são salvos com sucesso no banco de dados, você pode publicar os dois eventos que foram gerados.
O AccountDebitedEvent removerá o dinheiro da conta do pagador (atualiza o estado agregado e quaisquer modelos de exibição / projeção relacionados)
MoneyTransferEvent inicia o Saga / Process Manager.
O trabalho do gerente da saga / processo será tentar creditar a conta do beneficiário; se falhar, será necessário creditar o saldo de volta ao pagador.
O gerente da Saga / Process publicará um CreditAccountCommand que será aplicado à conta do beneficiário e, se for bem-sucedido, o AccountCreditedEvent será gerado.
Do ponto de vista da fonte de eventos, se você quiser reverter essa ação, todos os eventos nesta transação terão o ID de correlação / causalidade como o TransferMoneyCommand original, que você pode usar para gerar eventos para operações de desfazer / reversões.