A referência por ID entre Agregados leva a um Modelo de Domínio Anêmico?

5

Visão geral

Estou tentando entender a maneira mais prática de vincular raízes agregadas sem transferir muita lógica de negócios das Entidades / RA's para Serviços em todos, enquanto ainda aderindo à ponta Vaughn Vernon para:

Prefer references to external Aggregates only by their globally unique identity, not by holding a direct object reference (or “pointer”) .....

Detalhes

Vamos pegar, por exemplo, um sistema de pesquisa simples. Esse sistema de pesquisa permite que os usuários realizem uma pesquisa independente para um cliente contra uma lista de pessoas suspeitas.

Ele também permite realizar pesquisas por lote , que são apenas uma coleção de pesquisas em uma lista de clientes

Eu modelei o acima assim.

Emessência,o

  • Pesquisar
  • Pesquisaemlotes

sãoambasRaízesAgregadas.Umapesquisapodeserexecutadaindependentemente,mastambémpodeserexecutadacomopartedeumapesquisaemlotes.Nessecaso,apesquisaemlotecriada>Pesquisas.

Codificarissoébastantesimples

class BatchSearch { constructor(customers) { this.searches = [] this.customers = customers } run() { for (customer of this.customers) { const search = new Search(customer) search.run() this.searches.push(search) } this.markAsCompleted() } ... }

No entanto, Vaughn Vernon afirma que manter uma referência direta entre Aggregate Roots é um design ruim.

De Design agregado efetivo Parte II: fazer com que os agregados trabalhem juntos :

Prefer references to external Aggregates only by their globally unique identity, not by holding a direct object reference (or “pointer”) ..... Use a Repository or Domain Service (7) to look up dependent objects ahead of invoking the Aggregate behavior

Pelo que entendi, ele defende a transferência de interações entre Agregados em Serviços da seguinte forma:

class BatchSearchService {
  constructor() {
  }

  createBatchSearch(customers) {
    let searches = []
    const batchSearch = new BatchSearch()

    for (customer of customers) {
      const search = new Search(customer)
      // We link this Search with this Batch Search by ID only, here
      search.setBatchSearchId(batchSearch.getId())
      search.run()
      searches.push(search)
    }

    batchSearch.markAsCompleted()

    batchSearchRepo.save(batchSearch)
    searchRepo.save(searches)
  }
}

Essa recomendação não leva invariavelmente a um Modelo de domínio anêmico ?

AFAIK OOP é fundamentalmente o acoplamento de dados e operações em uma classe, mas pelo que entendi neste cenário, as operações são movidas da Classe de pesquisa em lote para o Serviço de pesquisa em lote , deixando a Classe de pesquisa em lote para armazenar apenas os dados

    
por Nik Kyriakides 20.08.2017 / 21:58
fonte

3 respostas

8

A recomendação de Vaughn Vernon é uma das melhores regras táticas que se deve seguir. Se você realmente precisar de uma referência a outra raiz agregada, precisará revisar os limites do seu agregado, pois provavelmente eles estão errados.

Nesse caso, o modelo é anêmico porque o negócio é anêmico , ele não tem quaisquer invariantes que ele precise proteger, pelo menos do que você apresentou. Em qualquer caso, aggregates deve ser usado no lado de gravação / comando da sua arquitetura; Estou especificando isso porque, em quase todos os domínios, "pesquisa" é uma operação do lado da consulta / consulta, mas suponho que seu domínio seja especial, em que uma "pesquisa" implica alguma mutação de estado.

    
por 21.08.2017 / 06:20
fonte
1

Eu não acho que você tenha duas raízes agregadas aqui. Uma raiz agregada é uma entidade - o que significa que ela tem "continuidade e identidade" no nível do domínio. De sua descrição, comentários e dos exemplos de código, parece que apenas o tipo Search é realmente uma entidade. O próprio BatchSearch parece não ter essa identidade (pelo menos, tal ID não aparece na primeira versão do código), o que provavelmente faz dele um serviço (DDD), especialmente se ele não tiver um estado visível externamente. Também pode ser tratado como um objeto de valor, dependendo de como você o usa. Agora, se for um serviço, observe que isso não significa que você deve mover a funcionalidade para algum método de utilitário; serviços podem ser implementados como objetos (e ser polimórfico, ter dependências, etc).

Os pontos de Vaughn Vernon são sobre carregar e modificar o estado dos agregados. Um agregado é um grupo de entidades relacionadas e objetos de valor que tem uma entidade > visível externamente (a raiz agregada - essencialmente um Fachada , que serve a dois propósitos: (1) define as regras de travessia e oculta a complexidade e (2) fornece algumas garantias sobre a consistência do agregado como um todo quando ele é modificado. No artigo ao qual você está vinculado, a questão gira em torno da ideia de que dois agregados associados não devem ser modificados em uma única operação atômica, pois isso tem certas implicações de design e desempenho (supondo que a meta seja a consistência atômica).

Portanto, para evitar essa situação, ele recomenda referenciar o outro agregado (que, novamente, é uma entidade) por seu ID de domínio. Isso não impede totalmente o cenário problemático, mas dificulta a sua realização, enquanto ainda permite a navegação para o outro agregado. Há também o benefício adicional da redução do consumo de memória, no sentido de que os dois agregados são carregados independentemente.

Na seção Navegação do modelo, ele fala sobre como você pode usar uma forma de carregamento lento para dar suporte à navegação entre raízes agregadas e recomenda "usar um repositório ou serviço de domínio para procurar objetos dependentes antes de invocar o comportamento agregado ", ao contrário de procurar dependências através de um repositório de dentro de um agregado, que é realmente apenas uma aplicação do princípio de inversão de dependência (onde este" repositório ou serviço de domínio "é usado para executar manualmente a injeção de dependência).

Ele ainda diz que se essa abordagem simplesmente não funcionar, então você deve considerar projetar seu sistema para consistência eventual (isto é, o sistema não deve assumir que uma atualização bem-sucedida de um agregado significa que um agregado relacionado também foi modificado de acordo e está em um estado válido naquele momento) e, em seguida, descreve uma abordagem baseada em evento para construir um sistema desse tipo.

Agora, a pergunta é: isso se aplica à sua classe BatchSearch ? Possui uma identidade de nível de domínio não transitória? É melhor modelado como um serviço que constrói e retorna um monte de entidades de domínio ( Search instances)? Além disso, o próprio artigo aponta que há casos em que você pode decidir quebrar as regras e relaciona várias razões para fazê-lo, o que também é algo a ser considerado.

    
por 26.08.2017 / 21:11
fonte
0

Uma maneira de unificar o modelo seria usar Pesquisar como raiz agregada com o modelo Invocação como parte do agregado.

Não é diferente de como você já modelou, mas pensar em Search e BatchSearch como dois modelos agregados diferentes precisa desaparecer.

Armazene Pesquisar como um recurso com sinalizadores que representam o estado como em lote ou não.

class Search {
    List<Invocation> invocationList;
    boolean: batched; 

    // you can check size of the list to determine whether it's batched or not - this simplifies the model further
}

class Invocation {
    List<SearchParameters> searchParameterList;

    ..
    //add more fields as required
    .. 
}

Projete seus casos de uso e modele-os em torno de tratar cada chamada como Pesquisa com um número diferente de chamadas (dependendo dos casos de uso, você pode nomear "Invocação" de maneira diferente).

Assim, os modelos permaneceriam dentro do limite da raiz agregada no contexto dado.

Espero que ajude. Deixe-me saber se preciso explicar qualquer problema de projeto perdido.

    
por 25.08.2017 / 19:43
fonte