Como tratar a validação de referências entre agregados?

11

Estou lutando um pouco com a referência entre agregados. Vamos supor que o agregado Cartenha uma referência ao agregado Driver. Essa referência será modelada por ter Car.driverId.

Agora, meu problema é até que ponto devo validar a criação de um Caragregado CarFactory. Devo confiar que o passado DriverIdse refere a um existente Driver ou devo verificar esse invariante?

Para verificar, vejo duas possibilidades:

  • Eu poderia mudar a assinatura da fábrica de automóveis para aceitar uma entidade motorista completa. A fábrica então selecionaria o ID dessa entidade e montaria o carro com isso. Aqui a invariante é verificada implicitamente.
  • Eu poderia ter uma referência de DriverRepositoryna CarFactorychamada e explicitamente driverRepository.exists(driverId).

Mas agora estou me perguntando, isso não é muita verificação invariável? Eu poderia imaginar que esses agregados poderiam viver em um contexto limitado separado, e agora eu poluiria o carro BC com dependências no DriverRepository ou na entidade Driver do driver BC.

Além disso, se eu conversasse com especialistas em domínio, eles nunca questionariam a validade de tais referências. Estou sentindo que poluo meu modelo de domínio com preocupações não relacionadas. Mas, novamente, em algum momento a entrada do usuário deve ser validada.

Markus Malkusch
fonte

Respostas:

6

Eu poderia mudar a assinatura da fábrica de automóveis para aceitar uma entidade motorista completa. A fábrica então selecionaria o ID dessa entidade e montaria o carro com isso. Aqui a invariante é verificada implicitamente.

Essa abordagem é atraente, pois você recebe o cheque gratuitamente e está bem alinhado com o idioma onipresente. A Carnão é movido por a driverId, mas por a Driver.

Essa abordagem é de fato usada por Vaughn Vernon no contexto limitado de amostra Identity & Access, onde ele passa um Useragregado para um Groupagregado, mas o Groupúnico se mantém em um tipo de valor GroupMember. Como você pode ver, isso também permite que ele verifique a ativação do usuário (estamos cientes de que a verificação pode estar obsoleta).

    public void addUser(User aUser) {
        //original code omitted
        this.assertArgumentTrue(aUser.isEnabled(), "User is not enabled.");

        if (this.groupMembers().add(aUser.toGroupMember()) && !this.isInternalGroup()) {
            //original code omitted
        }
    }

No entanto, ao passar a Driverinstância, você também se abre para uma modificação acidental do Driverinterior Car. A passagem de referência de valor facilita a discussão sobre mudanças do ponto de vista de um programador, mas, ao mesmo tempo, o DDD é sobre a Linguagem Ubíqua, portanto vale a pena arriscar.

Se você realmente consegue criar bons nomes para aplicar o Princípio de Segregação de Interface (ISP) , pode confiar em uma interface que não possui os métodos comportamentais. Talvez você também possa criar um conceito de objeto de valor que represente uma referência imutável do driver e que só possa ser instanciado a partir de um driver existente (por exemplo DriverDescriptor driver = driver.descriptor()).

Eu poderia imaginar que esses agregados poderiam viver em um contexto limitado separado, e agora eu poluiria o carro BC com dependências no DriverRepository ou na entidade Driver do driver BC.

Não, você não faria. Sempre existe uma camada anticorrupção para garantir que os conceitos de um contexto não se espalhem por outro. Na verdade, é muito mais fácil se você tiver um BC dedicado a associações de motorista de carro, pois é possível modelar conceitos existentes, como Care Driverespecificamente para esse contexto.

Portanto, você pode ter um DriverLookupServiceresponsável definido no BC para gerenciar associações de motorista de carro. Este serviço pode chamar um serviço da Web exposto pelo contexto de Gerenciamento de Driver, que retorna Driverinstâncias que provavelmente serão objetos de valor nesse contexto.

Observe que os serviços da web não são necessariamente o melhor método de integração entre BCs. Você também pode confiar em mensagens em que, por exemplo, uma UserCreatedmensagem do contexto de Gerenciamento de Driver seria consumida em um contexto remoto que armazenaria uma representação do driver em seu próprio banco de dados. O DriverLookupServiceusuário poderia então usar esse banco de dados e os dados do driver seriam mantidos atualizados com mais mensagens (por exemplo DriverLicenceRevoked).

Não sei dizer qual abordagem é melhor para o seu domínio, mas espero que isso lhe dê informações suficientes para tomar uma decisão.

plalx
fonte
3

A maneira como você faz a pergunta (e propõe duas alternativas) é como se a única preocupação fosse que o driverId ainda fosse válido no momento em que o carro foi criado.

No entanto, você também deve se preocupar que o driver associado ao driverId não seja excluído antes que o carro seja excluído ou entregue a outro motorista (e possivelmente também que o driver não esteja atribuído a outro carro (isso se o domínio restringir o motorista a apenas ser associado a um carro)).

Sugiro que, em vez de validação, você aloque (o que incluiria validação de presença). Você não permitirá exclusões enquanto ainda estiver alocado, protegendo assim contra a condição de corrida de dados obsoletos durante a construção, bem como outro problema de longo prazo. (Observe que a alocação valida e as marcas alocadas e opera atomicamente.)

Por outro lado, concordo com o @PriceJones que a associação entre o carro e o motorista provavelmente é uma responsabilidade separada do carro ou do motorista. Esse tipo de associação só aumentará em complexidade ao longo do tempo, porque soa como um problema de agendamento (motoristas, carros, horários / janelas, substitutos, etc.). Mesmo que pareça um problema de registro, pode-se querer histórico registros, bem como registros atuais. Assim, pode muito bem merecer seu próprio BC imediatamente.

Você pode fornecer um esquema de alocação (como uma contagem booleana ou de referência) dentro do BC das entidades agregadas que estão sendo alocadas ou dentro de um BC separado, por exemplo, aquele responsável por fazer a associação entre carro e motorista. Se você fizer o primeiro, poderá permitir operações de exclusão (válidas) emitidas para o carro ou motorista BC; se você fizer o último, precisará impedir exclusões dos BCs de carros e drivers e enviá-las pelo agendador de associações de carros e drivers.

Você também pode dividir algumas das responsabilidades de alocação entre os BCs, como segue. O BC de carro e motorista fornece um esquema de "alocação" que valida e define o booleano alocado com esse BC; quando sua alocação booleana é definida, o BC impede a exclusão das entidades correspondentes. (E o sistema está configurado para que o carro e o motorista BC só permitam alocação e desalocação do agendamento da associação de carro / motorista BC.)

O BC do carro e motorista mantém um calendário de condutores associados ao carro por alguns períodos / durações, agora e no futuro, e notifica os outros BCs de desalocação apenas no último uso de um carro ou motorista programado.


Como uma solução mais radical, você pode tratar os BCs de automóveis e condutores como fábricas de registros históricos apenas anexados, deixando a propriedade para o planejador da associação de carros e condutores. O carro BC pode gerar um carro novo, completo com todos os detalhes do carro, junto com seu VIN. A propriedade do carro é gerenciada pelo agendador da associação carro / motorista. Mesmo que uma associação de carro / motorista seja excluída e o próprio carro seja destruído, os registros do carro ainda existem no carro BC, por definição, e podemos usar o carro BC para procurar dados históricos; enquanto associações / proprietários de automóveis / condutores (passado, presente e potencialmente programados para o futuro) estão sendo administrados por outro BC.

Erik Eidt
fonte
2

Vamos supor que o carro agregado tenha uma referência ao driver agregado. Essa referência será modelada com Car.driverId.

Sim, esse é o caminho certo para unir um agregado a outro.

se eu conversasse com especialistas em domínio, eles nunca questionariam a validade de tais referências

Não é a pergunta certa para perguntar a seus especialistas em domínio. Tente "qual é o custo para os negócios se o driver não existir?"

Eu provavelmente não usaria o DriverRepository para verificar o driverId. Em vez disso, eu usaria um serviço de domínio para fazer isso. Eu acho que é melhor expressar a intenção - nos bastidores, o serviço de domínio ainda verifica o sistema de registro.

Então, algo como

class DriverService {
    private final DriverRepository driverRepository;

    boolean doesDriverExist(DriverId driverId) {
        return driverRepository.exists(driverId);
    }
}

Você realmente consulta o domínio sobre o driverId em vários pontos diferentes

  • Do cliente, antes de enviar o comando
  • No aplicativo, antes de passar o comando para o modelo
  • Dentro do modelo de domínio, durante o processamento de comandos

Qualquer uma ou todas essas verificações podem reduzir erros na entrada do usuário. Mas eles estão todos trabalhando com dados obsoletos; o outro agregado pode mudar imediatamente após fazermos a pergunta. Portanto, sempre há algum risco de falsos negativos / positivos.

  • Em um relatório de exceção, execute após a conclusão do comando

Aqui, você ainda está trabalhando com dados obsoletos (os agregados podem estar executando comandos enquanto você executa o relatório, talvez não seja possível ver as gravações mais recentes em todos os agregados). Mas as verificações entre agregados nunca serão perfeitas (Car.create (driver: 7) sendo executado ao mesmo tempo que Driver.delete (driver: 7)). Isso oferece uma camada extra de defesa contra riscos.

VoiceOfUnreason
fonte
1
Driver.deletenão deveria existir. Eu realmente nunca vi um domínio em que agregados são destruídos. Ao manter os ARs próximos, você nunca pode acabar com órfãos.
Plalx 30/05
1

Pode ser útil perguntar: Você tem certeza de que os carros são fabricados com motoristas? Eu nunca ouvi falar de um carro composto por um motorista no mundo real. A razão pela qual essa pergunta é importante é porque ela pode indicar a direção de criar carros e motoristas de forma independente e criar algum mecanismo externo que atribua um motorista a um carro. Um carro pode existir sem uma referência ao motorista e ainda ser um carro válido.

Se um carro tiver absolutamente um motorista em seu contexto, convém considerar o padrão do construtor. Esse padrão será responsável por garantir que os carros sejam construídos com os motoristas existentes. As fábricas atenderão carros e motoristas validados de forma independente, mas o construtor garantirá que o carro tenha a referência necessária antes de servir o carro.

Preço Jones
fonte
Também pensei na relação carro / motorista - mas a introdução de um agregado DriverAssignment apenas move a referência que precisa ser validada.
VoiceOfUnreason
1

Mas agora estou me perguntando, isso não é muita verificação invariável?

Acho que sim. A busca de um determinado DriverId no banco de dados retorna um conjunto vazio, se ele não existir. Portanto, verificar o resultado do retorno torna desnecessário perguntar se ele existe (e depois buscar).

Então o design da classe também torna desnecessário

  • Se houver um requisito "um carro estacionado pode ou não ter um motorista"
  • Se um objeto Driver requer um DriverIde é definido no construtor.
  • Se as Carnecessidades apenas o DriverId, ter um Driver.Idgetter. Sem setter.

Repositório não é o lugar para regras de negócios

  • A Carse importa se tiver um Driver(ou seu ID, pelo menos). A Driverse importa se tiver a DriverId. Ele se Repositorypreocupa com a integridade dos dados e não pode se importar menos com carros sem motorista.
  • O banco de dados terá regras de integridade de dados. Chaves não nulas, restrições não nulas, etc. Mas a integridade dos dados diz respeito ao esquema de dados / tabela, não às regras de negócios. Temos uma relação simbiótica fortemente correlacionada nesse caso, mas não misture os dois.
  • O fato de a DriverIdser um domínio de negócios é tratado nas classes apropriadas.

Violação de Separação de Preocupações

... acontece quando Repository.DriverIdExists()faz a pergunta.

Crie um objeto de domínio. Se não for um Driver, talvez um objeto DriverInfo(apenas um DriverIde Name, digamos). O DriverIdé validado na construção. Ele deve existir e ser do tipo certo e qualquer outra coisa. Em seguida, é um problema de design da classe do cliente como lidar com um driver / driverId inexistente.

Talvez um Caresteja bem sem motorista até você ligar Car.Drive(). Nesse caso, o Carobjeto, é claro, garante seu próprio estado. Não é possível dirigir sem um Driver- bem, ainda não.

Separar uma propriedade de sua classe é ruim

Claro, tenha um, Car.DriverIdse desejar. Mas deve ser algo como isto:

public class Car {
    // Non-null driver has a driverId by definition/contract.
    protected DriverInfo myDriver;
    public DriverId {get { return myDriver.Id; }}

    public void Drive() {
       if (myDriver == null ) return errorMessage; // or something
       // ... continue driving
    }
}

Isso não:

public class Car {
    public int DriverId {get; protected set;}
}

Agora, é Carpreciso lidar com todos os DriverIdproblemas de validade - uma única violação do princípio de responsabilidade; e código redundante provavelmente.

radarbob
fonte