Os agregados DDD são realmente uma boa ideia em um aplicativo da Web?

40

Estou mergulhando no Domain Driven Design e alguns dos conceitos que estou me deparando fazem muito sentido na superfície, mas quando penso mais sobre eles, tenho que me perguntar se essa é realmente uma boa ideia.

O conceito de agregados, por exemplo, faz sentido. Você cria pequenos domínios de propriedade para não precisar lidar com todo o modelo de domínio.

No entanto, quando penso sobre isso no contexto de um aplicativo Web, frequentemente acessamos o banco de dados para recuperar pequenos subconjuntos de dados. Por exemplo, uma página pode listar apenas o número de pedidos, com links para clicar para abrir o pedido e ver seus IDs de pedidos.

Se eu sou Agregados entendimento correto, eu normalmente usaria o padrão de repositório para retornar um OrderAggregate que conteria os membros GetAll, GetByID, Delete, e Save. OK isso parece bom. Mas...

Se eu chamar GetAll para listar todos os meus pedidos, parece-me que esse padrão exigiria a devolução de toda a lista de informações agregadas, pedidos completos, linhas de pedidos, etc ... Quando eu precisar apenas de um pequeno subconjunto dessas informações (apenas informações do cabeçalho).

Estou esquecendo de algo? Ou existe algum nível de otimização que você usaria aqui? Não consigo imaginar que alguém advogaria o retorno de agregados inteiros de informações quando você não precisar.

Certamente, pode-se criar métodos no seu repositório GetOrderHeaders, mas isso parece anular o propósito de usar um repositório como o padrão em primeiro lugar.

Alguém pode esclarecer isso pra mim?

EDITAR:

Depois de muito mais pesquisa, acho que a desconexão aqui é que um padrão de Repositório puro é diferente do que a maioria das pessoas pensa que um Repositório é.

Fowler define um repositório como um armazenamento de dados que usa semântica de coleção e geralmente é mantido na memória. Isso significa criar um gráfico de objeto inteiro.

Evans altera o Repositório para incluir Raízes Agregadas e, portanto, o repositório é amputado para suportar apenas os objetos em um Agregado.

A maioria das pessoas parece pensar nos repositórios como objetos de acesso a dados glorificados, onde você apenas cria métodos para obter os dados que deseja. Essa não parece ser a intenção, conforme descrito em Patterns of Enterprise Application Architecture de Fowler.

Outros ainda pensam em um repositório como uma abstração simples usada principalmente para facilitar testes e zombarias, ou para desacoplar a persistência do resto do sistema.

Acho que a resposta é que esse é um conceito muito mais complexo do que eu pensava.

Erik Funkenbusch
fonte
4
"Acho que a resposta é que esse é um conceito muito mais complexo do que eu pensava que era". Isto é muito verdadeiro.
Quentin-starin
para a sua situação, você pode criar um proxy para o objeto raiz agregada que recupera seletivamente e armazena em cache os dados apenas quando é solicitado
Steven A. Lowe
Minha sugestão é implementar a carga lenta nas associações agregadas raiz. Assim, você pode recuperar uma lista de raízes sem carregar muitos objetos.
Juliano
4
quase 6 anos depois, ainda é uma boa pergunta. Depois de ler o capítulo no livro vermelho, eu diria: não faça seus agregados grandes demais. É tentador escolher algum conceito de nível superior do seu domínio e declarar a raiz para governá-los todos, exceto o DDD defende agregados menores. E atenua ineficiências como as descritas acima.
CPT. Senkfuss
Seus agregados devem ser o menor possível e, ao mesmo tempo, serem naturais e eficazes para o domínio (um desafio!). Além disso, é perfeitamente adequado e desejável que seus repositórios tenham métodos altamente específicos .
Timo

Respostas:

30

Não use seu Modelo de Domínio e agregados para consulta.

De fato, o que você está perguntando é uma pergunta bastante comum e foi estabelecido um conjunto de princípios e padrões para evitar exatamente isso. É chamado CQRS .

quentin-starin
fonte
2
@Mystere Man: Não, é para fornecer os dados mínimos necessários. Esse é um dos grandes objetivos de um modelo de leitura separado. Também ajuda a resolver alguns problemas de concorrência antecipadamente. O CQRS possui vários benefícios quando aplicado ao DDD.
Quentin-starin
2
@ Mystere: Estou bastante surpreso que você sentiria falta disso se ler o artigo ao qual vinculei. Consulte a seção "Consultas (relatórios)": "Quando um aplicativo está solicitando dados ... isso deve ser feito em uma única chamada para a camada Query e, em troca, ele obterá um único DTO contendo todos os dados necessários. A razão para isso é que os dados normalmente são consultados muitas vezes mais do que o comportamento do domínio está sendo executado; portanto, ao otimizar isso, você aprimorará o desempenho percebido do sistema. "
Quentin-starin
4
É por isso que não usaríamos um repositório para ler dados em um sistema CQRS. Escreveríamos uma classe simples para encapsular uma consulta (usando qualquer tecnologia que fosse conveniente ou necessária, geralmente ADO.Net ou Linq2Sql ou SubSonic se encaixa bem aqui), retornando apenas (todos) os dados necessários para a tarefa em questão, para evitar arrastando dados por todas as camadas normais de um repositório DDD. Somente usaríamos um Repositório para recuperar um Agregado se desejássemos enviar um comando para o domínio.
Quentin-starin
9
"Não consigo imaginar que alguém advogaria o retorno de agregados inteiros de informações quando você não precisar". Estou tentando dizer que você está exatamente correto com esta afirmação. Não recupere um agregado inteiro de informações quando não precisar delas. Este é o núcleo do CQRS aplicado ao DDD. Você não precisa de um agregado para consultar. Obtenha os dados através de um mecanismo diferente e faça isso de forma consistente.
Quentin-starin
2
@qes De fato, a melhor solução é não usar DDD para consultas (leia-se) :) Mas você ainda usa DDD na parte de comando, ou seja, para armazenar ou atualizar dados. Então, eu tenho uma pergunta para você: você sempre usa Repositórios com Entidades quando precisa atualizar dados no banco de dados? Digamos que você precise alterar apenas um valor pequeno na coluna (alternar de algum tipo), ainda carrega Entidade inteira na camada App, altera um valor (propriedade) e salva a Entidade inteira novamente no DB? Um pouco de exagero também?
Andrew
8

Lutei e ainda estou lutando com a melhor maneira de usar o padrão de repositório em um Design Orientado a Domínio. Depois de usá-lo agora pela primeira vez, criei as seguintes práticas:

  1. Um repositório deve ser simples; é responsável apenas por armazenar objetos de domínio e recuperá-los. Toda outra lógica deve estar em outros objetos, como fábricas e serviços de domínio.

  2. Um repositório se comporta como uma coleção como se fosse uma coleção de raízes agregadas na memória.

  3. Um repositório não é um DAO genérico, cada repositório tem sua interface exclusiva e estreita. Um repositório geralmente possui métodos de busca específicos que permitem pesquisar a coleção em termos de domínio (por exemplo: dê-me todos os pedidos abertos para o usuário X). O próprio repositório pode ser implementado com a ajuda de um DAO genérico.

  4. Idealmente, os métodos localizadores retornarão apenas raízes agregadas. Se isso for ineficiente, ele também poderá retornar objetos de valor somente leitura que contenham exatamente o que você precisa (embora seja uma vantagem se esses objetos de valor também puderem ser expressos em termos de domínio). Como último recurso, o repositório também pode ser usado para retornar subconjuntos ou coleções de subconjuntos de uma raiz agregada.

  5. Escolhas como essas dependem das tecnologias usadas, pois você precisa encontrar uma maneira de expressar com mais eficiência seu modelo de domínio com as tecnologias usadas.

Kdeveloper
fonte
É definitivamente um assunto complexo, com certeza. É difícil transformar a teoria em prática, especialmente quando ela combina duas teorias distintas e separadas em uma única prática.
Sebastian Patten
6

Eu não acho que seu método GetOrderHeaders derrote o objetivo do repositório.

O DDD está preocupado (entre outras coisas) em garantir que você obtenha o que precisa por meio da raiz agregada (você não teria um OrderDetailsRepository, por exemplo), mas isso não o limita da maneira que você está mencionando.

Se um OrderHeader é um conceito de Domínio, você deve defini-lo como tal e ter os métodos de repositório apropriados para recuperá-los. Apenas certifique-se de estar passando pela raiz agregada correta ao fazê-lo.

Eric King
fonte
Talvez eu esteja confundindo conceitos aqui, mas meu entendimento do padrão do repositório é dissociar a persistência do domínio, usando uma interface padrão para persistência. Se você precisar adicionar métodos personalizados para um recurso específico, isso parece estar unindo as coisas novamente.
Erik Funkenbusch
11
O mecanismo de persistência é dissociado do domínio, mas não o que está sendo persistido. Se você estiver dizendo coisas como "precisamos listar os cabeçalhos de pedidos aqui", será necessário modelar o OrderHeader no seu domínio e fornecer uma maneira de recuperá-los do seu repositório.
Eric King
Além disso, não se desligue da "interface padrão de persistência". Não existe um padrão genérico de repositório que seja suficiente para todos os aplicativos possíveis. Cada aplicativo terá muitos métodos de repositório além do padrão "GetById", "Salvar" etc. Esses métodos são o ponto de partida, não o ponto final.
Eric King
4

Meu uso do DDD pode não ser considerado DDD "puro", mas adaptei as seguintes estratégias do mundo real usando o DDD em um armazenamento de dados do banco de dados.

  • Uma raiz agregada possui um repositório associado
  • O repositório associado é usado apenas por essa raiz agregada (não está disponível ao público)
  • Um repositório pode conter chamadas de consulta (por exemplo, GetAllActiveOrders, GetOrderItemsForOrder)
  • Um serviço expõe um subconjunto público do repositório e outras operações não brutas (por exemplo, Transferir dinheiro de uma conta bancária para outra, LoadById, Search / Find, CreateEntity etc.).
  • Eu uso a pilha Raiz -> Serviço -> Repositório. Supõe-se que um serviço DDD seja usado para qualquer coisa que uma Entidade não possa responder por si mesma (por exemplo, LoadById, TransferMoneyFromAccountToAccount), mas no mundo real eu também uso outros serviços relacionados a CRUD (Salvar, Excluir, Consultar), mesmo que o O root deve ser capaz de "responder / executar" eles mesmos. Observe que não há nada errado em conceder a uma entidade acesso a outro serviço raiz agregado! No entanto, lembre-se de que você não incluiria em um serviço (GetOrderItemsForOrder), mas o incluiria no repositório para que a Raiz Agregada possa utilizá-lo. Observe que um serviço não deve expor nenhuma consulta aberta como o repositório.
  • Normalmente, defino um Repositório de maneira abstrata no modelo de domínio (via interface) e forneço uma implementação concreta separada. Eu defino completamente um serviço no modelo de domínio, injetando em um repositório concreto para seu uso.

** Você não precisa trazer de volta um agregado inteiro. No entanto, se você quiser mais, peça à raiz, não a algum outro serviço ou repositório. Este é um carregamento lento e pode ser feito manualmente com um carregamento lento e inadequado (injetando o repositório / serviço apropriado na raiz) ou usando um ORM que suporte isso.

No seu exemplo, eu provavelmente forneceria uma chamada de repositório que trouxesse apenas os cabeçalhos dos pedidos se quisesse carregar os detalhes em uma chamada separada. Observe que, ao termos um "OrderHeader", estamos realmente introduzindo um conceito adicional no domínio.

Mike Rowley
fonte
3

Seu modelo de domínio contém sua lógica de negócios em sua forma mais pura. Todos os relacionamentos e operações que suportam operações de negócios. O que está faltando em seu mapa conceitual é a idéia da Camada de Serviço de Aplicativo, a camada de serviço envolve o modelo de domínio e fornece uma visão simplificada do domínio comercial (uma projeção, se você desejar) que permite que o modelo de Domínio seja alterado conforme necessário sem impactar diretamente os aplicativos usando a camada de serviço.

Indo além. A ideia do agregado é que exista um objeto, a raiz agregada, responsável por manter a consistência do agregado. No seu exemplo, o pedido seria responsável por manipular suas linhas de pedido.

Para o seu exemplo, a camada de serviço exporia uma operação como GetOrdersForCustomer que retornaria apenas o necessário para exibir uma lista resumida dos pedidos (como você os chama OrderHeaders).

Finalmente, o padrão de repositório não é apenas uma coleção, mas também permite consultas declarativas. No C #, você pode usar o LINQ como objeto de consulta , ou a maioria dos outros O / RMs também fornece uma especificação de objeto de consulta.

Um Repositório medeia entre o domínio e as camadas de mapeamento de dados, agindo como uma coleção de objetos de domínio na memória. Os objetos do cliente constroem as especificações da consulta declarativamente e as enviam ao Repositório para satisfação. (da página do Repositório de Fowler )

Como você pode criar consultas no repositório, também faz sentido fornecer métodos de conveniência que lidam com consultas comuns. Ou seja, se você quiser apenas os cabeçalhos do seu pedido, poderá criar uma consulta que retorne apenas o cabeçalho e expô-lo a partir de um método de conveniência em seus repositórios.

Espero que isso ajude a esclarecer as coisas.

Michael Brown
fonte
0

Sei que essa é uma pergunta antiga, mas parece que cheguei a uma resposta diferente.

Quando eu faço um Repositório, ele geralmente envolve algumas consultas em cache .

Fowler define um repositório como um armazenamento de dados que usa semântica de coleção e geralmente é mantido na memória. Isso significa criar um gráfico de objeto inteiro.

Mantenha esses repositórios em seus servidores ram. Eles não passam apenas objetos para o banco de dados!

Se eu estiver em um aplicativo Web com pedidos de listagem de páginas, nos quais você pode clicar para ver detalhes, é provável que eu queira que minha página de listagem de pedidos tenha detalhes sobre os pedidos (ID, Nome, Valor, Data) para ajudar um usuário a decidir para qual deles deseja procurar.

Neste ponto, você tem duas opções.

  1. Você pode consultar o banco de dados e recuperar exatamente o que precisa para fazer a listagem e, em seguida, consultar novamente para obter os detalhes individuais que você precisa ver na página de detalhes.

  2. Você pode fazer 1 consulta que retira todas as informações e as armazena em cache. Na próxima página, solicite a leitura dos servidores ram em vez do banco de dados. Se o usuário responder ou selecionar a próxima página, você ainda não fará nenhuma viagem ao banco de dados.

Na realidade, como você implementa é exatamente isso e detalhes da implementação. Se meu maior usuário tiver 10 pedidos, provavelmente eu quero ir com a opção 2. Se estou falando de 10.000 pedidos, a opção 1 é necessária. Nos dois casos acima e em muitos outros casos, quero que o repositório oculte esses detalhes de implementação.

A partir de agora, se eu receber um ticket para informar ao usuário quanto gastaram em pedidos ( dados agregados ) no último mês na página de listagem de pedidos, prefiro escrever a lógica para calcular isso no SQL e fazer mais uma ida e volta para o banco de dados ou você prefere calculá-lo usando os dados que já estão nos servidores ram?

Na minha experiência, os agregados de domínio oferecem enormes benefícios.

  • Eles são uma enorme reutilização de código de peça que realmente funciona.
  • Eles simplificam o código, mantendo a lógica de negócios correta na camada principal, em vez de precisar detalhar uma camada de infraestrutura para que o servidor sql faça isso.
  • Eles também podem acelerar drasticamente seus tempos de resposta, reduzindo o número de consultas que você precisa fazer, pois é possível armazená-las em cache facilmente.
  • O SQL que estou escrevendo geralmente é muito mais sustentável, já que geralmente estou pedindo tudo e calculando o lado do servidor.
WhiteleyJ
fonte