Em Domain Driven Design, parece haver lotes de acordo que as entidades não deve acessar repositórios diretamente.
Isso veio do livro de Eric Evans Domain Driven Design , ou veio de outro lugar?
Onde existem boas explicações para o raciocínio por trás disso?
editar: Para esclarecer: não estou falando sobre a prática clássica de OO de separar o acesso a dados em uma camada separada da lógica de negócios - estou falando sobre o arranjo específico pelo qual no DDD, as entidades não devem conversar com os dados camada de acesso (ou seja, eles não devem conter referências a objetos de repositório)
atualização: eu dei a recompensa ao BacceSR porque a resposta dele parecia mais próxima, mas ainda estou no escuro sobre isso. Se esse é um princípio tão importante, deve haver alguns bons artigos on-line em algum lugar, com certeza?
atualização: março de 2013, os votos positivos sobre a questão implicam muito interesse nisso, e mesmo tendo havido muitas respostas, ainda acho que há espaço para mais se as pessoas tiverem ideias sobre isso.
Respostas:
Há um pouco de confusão aqui. Repositórios acessam raízes agregadas. Raízes agregadas são entidades. A razão para isso é a separação de preocupações e boas camadas. Isso não faz sentido em pequenos projetos, mas se você estiver em uma equipe grande, deseja dizer: "Você acessa um produto através do Repositório do Produto. O produto é uma raiz agregada para uma coleção de entidades, incluindo o objeto ProductCatalog. Se você deseja atualizar o ProductCatalog, deve passar pelo ProductRepository. "
Dessa forma, você tem uma separação muito, muito clara na lógica de negócios e onde as coisas são atualizadas. Você não tem um garoto que está sozinho e escreve todo esse programa que faz todas essas coisas complicadas no catálogo de produtos e, quando se trata de integrá-lo ao projeto upstream, você fica sentado olhando e percebendo tudo tem que ser abandonado. Também significa que quando as pessoas se juntam à equipe, adicionam novos recursos, sabem para onde ir e como estruturar o programa.
Mas espere! Repositório também se refere à camada de persistência, como no Padrão do Repositório. Em um mundo melhor, o Repositório de Eric Evans e o Padrão de Repositório teriam nomes separados, porque eles tendem a se sobrepor bastante. Para obter o padrão do repositório, você contrasta com outras maneiras pelas quais os dados são acessados, com um barramento de serviço ou um sistema de modelo de evento. Normalmente, quando você chega a esse nível, a definição do Repositório de Eric Evans segue o caminho e você começa a falar sobre um contexto limitado. Cada contexto limitado é essencialmente sua própria aplicação. Você pode ter um sistema sofisticado de aprovação para inserir itens no catálogo de produtos. Em seu design original, o produto era a peça central, mas nesse contexto limitado o catálogo de produtos é. Você ainda pode acessar informações do produto e atualizar o produto por meio de um barramento de serviço,
Voltar à sua pergunta original. Se você estiver acessando um repositório de dentro de uma entidade, significa que a entidade não é realmente uma entidade comercial, mas provavelmente algo que deveria existir em uma camada de serviço. Isso ocorre porque as entidades são objetos de negócios e devem se preocupar em ser o mais parecido possível com uma DSL (linguagem específica de domínio). Só tem informações comerciais nesta camada. Se estiver solucionando um problema de desempenho, procure outro lugar, pois apenas as informações comerciais devem estar aqui. Se, de repente, você tem problemas com aplicativos aqui, está dificultando a extensão e a manutenção de um aplicativo, que é realmente o coração do DDD: criar software sustentável.
Resposta ao comentário 1 : Certo, boa pergunta. Portanto, nem toda validação ocorre na camada de domínio. A Sharp tem um atributo "DomainSignature" que faz o que você deseja. Ele reconhece a persistência, mas ser um atributo mantém a camada do domínio limpa. Ele garante que você não tenha uma entidade duplicada com, no seu exemplo, o mesmo nome.
Mas vamos falar sobre regras de validação mais complicadas. Digamos que você seja Amazon.com. Você já encomendou algo com um cartão de crédito vencido? Tenho, onde não atualizei o cartão e comprei algo. Ele aceita o pedido e a interface do usuário informa que tudo está bem. Cerca de 15 minutos depois, recebo um e-mail informando que há um problema com meu pedido, meu cartão de crédito é inválido. O que está acontecendo aqui é que, idealmente, há alguma validação de regex na camada de domínio. Esse é um número de cartão de crédito correto? Se sim, persista o pedido. No entanto, há uma validação adicional na camada de tarefas do aplicativo, onde um serviço externo é consultado para verificar se o pagamento pode ser feito no cartão de crédito. Caso contrário, não envie nada, suspenda o pedido e aguarde o cliente.
Não tenha medo de criar objetos de validação na camada de serviço que possa acessar repositórios. Apenas mantenha-o fora da camada de domínio.
fonte
No começo, eu era persuasivo em permitir que algumas de minhas entidades acessassem repositórios (ou seja, carregamento lento sem um ORM). Mais tarde, cheguei à conclusão de que não deveria e que poderia encontrar maneiras alternativas:
Vernon Vaughn no livro vermelho Implementing Domain-Driven Design refere-se a esse problema em dois lugares que conheço (nota: este livro é totalmente endossado por Evans, como você pode ler no prefácio). No Capítulo 7, em Serviços, ele usa um serviço de domínio e uma especificação para solucionar a necessidade de um agregado usar um repositório e outro agregado para determinar se um usuário é autenticado. Ele é citado dizendo:
Vernon, Vaughn (06-02-2013). Implementando o design orientado a domínio (local do Kindle 6089). Pearson Education. Edição Kindle.
E no capítulo 10 sobre agregados, na seção intitulada "Navegação do modelo", ele diz (logo após recomendar o uso de IDs globais exclusivos para fazer referência a outras raízes agregadas):
Ele mostra um exemplo disso no código:
Ele também menciona ainda outra solução de como um serviço de domínio pode ser usado em um método de comando Agregado, juntamente com o despacho duplo . (Não posso recomendar o suficiente como é benéfico ler o livro dele. Depois que você se cansar de vasculhar a Internet sem parar, garanta o merecido dinheiro e leia o livro.)
Tive então uma discussão com o sempre gracioso Marco Pivetta @Ocramius, que me mostrou um pouco de código sobre como extrair uma especificação do domínio e usá-la:
1) Isso não é recomendado:
2) Em um serviço de domínio, isso é bom:
fonte
getFriends()
antes de fazer qualquer outra coisa, ele fica vazio ou carregado preguiçosamente. Se vazio, esse objeto está em um estado inválido. Alguma idéia sobre isso?É uma pergunta muito boa. Aguardo ansiosamente alguma discussão sobre isso. Mas acho que isso é mencionado em vários livros sobre DDD, Jimmy Nilssons e Eric Evans. Eu acho que também é visível através de exemplos de como usar o padrão reposistório.
MAS vamos discutir. Eu acho que um pensamento muito válido é por que uma entidade deve saber como persistir em outra entidade? Importante no DDD é que cada entidade tem a responsabilidade de gerenciar sua própria "esfera de conhecimento" e não deve saber nada sobre como ler ou escrever outras entidades. Claro que você provavelmente pode adicionar uma interface de repositório à Entidade A para ler as Entidades B. Mas o risco é que você exponha o conhecimento de como persistir B. A entidade A também fará a validação em B antes de persistir em B no db?
Como você pode ver, a entidade A pode se envolver mais no ciclo de vida da entidade B e isso pode adicionar mais complexidade ao modelo.
Eu acho (sem nenhum exemplo) que o teste de unidade será mais complexo.
Mas tenho certeza de que sempre haverá cenários em que você ficará tentado a usar repositórios por meio de entidades. Você tem que olhar para cada cenário para fazer um julgamento válido. Prós e contras. Mas a solução de entidade de repositório, na minha opinião, começa com muitos contras. Deve ser um cenário muito especial com os profissionais que equilibram os contras ....
fonte
Por que separar o acesso a dados?
No livro, acho que as duas primeiras páginas do capítulo Design Orientado a Modelo fornecem algumas justificativas para o motivo de você querer abstrair os detalhes técnicos da implementação a partir da implementação do modelo de domínio.
Isso parece ser tudo com o objetivo de evitar um "modelo de análise" separado que se divorcia da implementação real do sistema.
Pelo que entendi do livro, ele diz que esse "modelo de análise" pode acabar sendo projetado sem considerar a implementação de software. Uma vez que os desenvolvedores tentam implementar o modelo entendido pelo lado comercial, eles formam suas próprias abstrações devido à necessidade, causando uma barreira na comunicação e no entendimento.
Na outra direção, os desenvolvedores que apresentam muitas preocupações técnicas no modelo de domínio também podem causar essa divisão.
Portanto, você pode considerar que praticar a separação de preocupações, como persistência, pode ajudar a proteger contra esses projetos e modelos de análise divergentes. Se for necessário introduzir coisas como persistência no modelo, é uma bandeira vermelha. Talvez o modelo não seja prático para implementação.
Citação:
"O modelo único reduz as chances de erro, porque o design agora é uma conseqüência direta do modelo cuidadosamente considerado. O design e até o próprio código têm a comunicabilidade de um modelo".
Da maneira como estou interpretando isso, se você tiver mais linhas de código lidando com coisas como acesso ao banco de dados, você perde essa capacidade de comunicação.
Se a necessidade de acessar um banco de dados é para coisas como verificar a exclusividade, dê uma olhada em:
Udi Dahan: os maiores erros que as equipes cometem ao aplicar DDD
http://gojko.net/2010/06/11/udi-dahan-the-biggest-mistakes-teams-make-when-applying-ddd/
em "Todas as regras não são criadas iguais"
e
Empregando o padrão de modelo de domínio
http://msdn.microsoft.com/en-us/magazine/ee236415.aspx#id0400119
em "Cenários para não usar o modelo de domínio", que aborda o mesmo assunto.
Como separar o acesso a dados
Carregando dados através de uma interface
A "camada de acesso a dados" foi abstraída por meio de uma interface, que você chama para recuperar os dados necessários:
Prós: A interface separa o código de canalização de "acesso a dados", permitindo que você ainda faça testes. O acesso a dados pode ser tratado caso a caso, permitindo melhor desempenho do que uma estratégia genérica.
Contras: O código de chamada deve assumir o que foi carregado e o que não foi.
Diga GetOrderLines retorna objetos OrderLine com uma propriedade ProductInfo nula por motivos de desempenho. O desenvolvedor deve ter um conhecimento profundo do código por trás da interface.
Eu tentei esse método em sistemas reais. Você acaba alterando o escopo do que é carregado o tempo todo, na tentativa de corrigir problemas de desempenho. Você acaba espiando por trás da interface para olhar o código de acesso a dados para ver o que está e não está sendo carregado.
Agora, a separação de preocupações deve permitir que o desenvolvedor se concentre em um aspecto do código ao mesmo tempo, o máximo possível. A técnica da interface remove o COMO esses dados são carregados, mas não QUANTO dados são carregados, QUANDO são carregados e ONDE são carregados.
Conclusão: Separação bastante baixa!
Carregamento lento
Os dados são carregados sob demanda. As chamadas para carregar dados estão ocultas no próprio gráfico do objeto, onde o acesso a uma propriedade pode fazer com que uma consulta sql seja executada antes de retornar o resultado.
Prós: O 'QUANDO, ONDE E COMO' do acesso a dados está oculto do desenvolvedor, focado na lógica do domínio. Não há código no agregado que lide com o carregamento de dados. A quantidade de dados carregados pode ser a quantidade exata exigida pelo código.
Contras: quando você é atingido por um problema de desempenho, é difícil corrigi-lo quando você tem uma solução genérica "tamanho único". O carregamento lento pode causar um desempenho pior no geral, e a implementação de um carregamento lento pode ser complicado.
Interface da função / busca ansiosa
Cada caso de uso é explicitado por meio de uma Interface de Função implementada pela classe agregada, permitindo que as estratégias de carregamento de dados sejam tratadas por caso de uso.
A estratégia de busca pode ficar assim:
Em seguida, seu agregado pode se parecer com:
O BillOrderFetchingStrategy é usado para criar o agregado e, em seguida, o agregado faz seu trabalho.
Prós: Permite código personalizado por caso de uso, permitindo o desempenho ideal. Está alinhado com o Princípio de Segregação de Interface . Não há requisitos de código complexos. Os testes de unidade agregados não precisam imitar a estratégia de carregamento. A estratégia de carregamento genérica pode ser usada na maioria dos casos (por exemplo, uma estratégia "carregar tudo") e estratégias especiais de carregamento podem ser implementadas quando necessário.
Contras: o desenvolvedor ainda precisa ajustar / revisar a estratégia de busca após alterar o código do domínio.
Com a abordagem da estratégia de busca, você ainda pode mudar o código de busca personalizado para alterar as regras de negócios. Não é uma separação perfeita de preocupações, mas acabará sendo mais sustentável e é melhor que a primeira opção. A estratégia de busca encapsula os dados HOW, WHEN e WHERE são carregados. Ele tem uma melhor separação de preocupações, sem perder a flexibilidade, pois o tamanho único se adapta a todas as abordagens de carregamento lento.
fonte
Eu encontrei este blog com bons argumentos contra o encapsulamento de repositórios dentro de entidades:
http://thinkbeforecoding.com/post/2009/03/04/How-not-to-inject-services-in-entities
fonte
Que pergunta excelente. Estou no mesmo caminho da descoberta, e a maioria das respostas na Internet parece trazer tantos problemas quanto soluções.
Então (com o risco de escrever algo que discordo daqui a um ano), aqui estão minhas descobertas até agora.
Antes de tudo, gostamos de um modelo de domínio rico , que nos oferece alta capacidade de descoberta (do que podemos fazer com um agregado) e legibilidade (chamadas de método expressivas).
Queremos conseguir isso sem injetar nenhum serviço no construtor de uma entidade, porque:
Como, então, podemos fazer isso? Minha conclusão até agora é que as dependências do método e o envio duplo fornecem uma solução decente.
CreateCreditNote()
agora requer um serviço responsável pela criação de notas de crédito. Ele usa expedição dupla , descarregando totalmente o trabalho para o serviço responsável, mantendo a capacidade de descoberta daInvoice
entidade.SetStatus()
agora tem uma dependência simples de um criador de logs, que obviamente fará parte do trabalho .Para o último, para facilitar as coisas no código do cliente, podemos fazer o logon em um
IInvoiceService
. Afinal, o registro de faturas parece bastante intrínseco a uma fatura. Esse tipo deIInvoiceService
ajuda a evitar a necessidade de todos os tipos de mini-serviços para várias operações. A desvantagem é que ele torna-se obscurecer o que exatamente que o serviço vai fazer . Pode até começar a parecer expedição dupla, enquanto a maior parte do trabalho ainda é realmente realizada porSetStatus()
si só.Ainda poderíamos nomear o parâmetro 'logger', na esperança de revelar nossa intenção. Parece um pouco fraco, no entanto.
Em vez disso, optaria por solicitar um
IInvoiceLogger
(como já fazemos no exemplo de código) eIInvoiceService
implementamos essa interface. O código do cliente pode simplesmente usar seu únicoIInvoiceService
para todos osInvoice
métodos que solicitam um 'mini-serviço' intrínseco de fatura muito particular, enquanto as assinaturas de método ainda deixam muito claro o que estão solicitando.Percebo que não endereçei repositórios de maneira exlitiva. Bem, o criador de logs é ou usa um repositório, mas deixe-me também fornecer um exemplo mais explícito. Podemos usar a mesma abordagem, se o repositório for necessário em apenas um método ou dois.
De fato, isso fornece uma alternativa às cargas preguiçosas sempre problemáticas .
Atualização: deixei o texto abaixo para fins históricos, mas sugiro evitar 100% de cargas preguiçosas.
Para os verdadeiros, cargas preguiçosos à base de propriedade, eu não uso atualmente injeção de construtor, mas de uma forma persistência-ignorante.
Por um lado, um repositório que carrega um
Invoice
do banco de dados pode ter acesso livre a uma função que carrega as notas de crédito correspondentes e injeta essa função noInvoice
.Por outro lado, o código que cria um novo real
Invoice
passa apenas uma função que retorna uma lista vazia:(Um costume
ILazy<out T>
poderia nos livrar do feio elencoIEnumerable
, mas isso complicaria a discussão.)Ficaria feliz em ouvir suas opiniões, preferências e melhorias!
fonte
Para mim, isso parece ser uma boa prática geral relacionada ao OOD, em vez de ser específica para o DDD.
Razões em que consigo pensar são:
fonte
simplesmente Vernon Vaughn fornece uma solução:
fonte
Aprendi a codificar a programação orientada a objetos antes que todo esse burburinho de camada separada aparecesse, e meus primeiros objetos / classes mapearam diretamente o banco de dados.
Eventualmente, adicionei uma camada intermediária porque tive que migrar para outro servidor de banco de dados. Eu já vi / ouvi sobre o mesmo cenário várias vezes.
Eu acho que separar o acesso a dados (também conhecido como "Repositório") da lógica de negócios é uma daquelas coisas que foram reinventadas várias vezes, por meio do livro Design de Domínio Dirigido, causando muito "ruído".
Atualmente, uso 3 camadas (GUI, Lógica, Acesso a Dados), como muitos desenvolvedores, porque é uma boa técnica.
A separação dos dados em uma
Repository
camada (também conhecida comoData Access
camada) pode ser vista como uma boa técnica de programação, e não apenas uma regra, a ser seguida.Como muitas metodologias, convém iniciar, NÃO implementado e, eventualmente, atualizar seu programa, depois de entendê-las.
Citação: A Ilíada não foi totalmente inventada por Homer, Carmina Burana não foi totalmente inventada por Carl Orff e, em ambos os casos, a pessoa que pôs os outros trabalhando, todos juntos, recebeu o crédito ;-)
fonte
É coisa velha. O livro de Eric apenas fez vibrar um pouco mais.
A razão é simples - a mente humana fica fraca quando enfrenta vários contextos vagamente relacionados. Eles levam à ambiguidade (América do Sul / América do Norte significa América do Sul / América do Norte), a ambiguidade leva ao mapeamento constante das informações sempre que a mente "toca" e isso resume-se a má produtividade e erros.
A lógica de negócios deve ser refletida o mais claramente possível. Chaves estrangeiras, normalização, mapeamento relacional de objetos são de domínio completamente diferente - essas coisas são técnicas e relacionadas ao computador.
Por analogia: se você está aprendendo a escrever à mão, não deve se preocupar em entender onde a caneta foi feita, por que a tinta permanece no papel, quando o papel foi inventado e quais são outras famosas invenções chinesas.
A razão ainda é a mesma que mencionei acima. Aqui está apenas um passo adiante. Por que as entidades devem ignorar parcialmente a persistência se elas podem ser (pelo menos próximas a) totalmente? Preocupações menos relacionadas ao domínio que nosso modelo mantém - mais espaço para respirar quando nossa mente precisa reinterpretá-la.
fonte
Para citar Carolina Lilientahl, "Os padrões devem evitar ciclos" https://www.youtube.com/watch?v=eJjadzMRQAk , em que ela se refere às dependências cíclicas entre as classes. No caso de repositórios dentro de agregados, existe a tentação de criar dependências cíclicas por conveniência da navegação de objetos como a única razão. O padrão mencionado acima pelo programa, recomendado por Vernon Vaughn, onde outros agregados são referenciados por IDs em vez de instâncias raiz (existe um nome para esse padrão?) Sugere uma alternativa que pode guiar outras soluções.
Exemplo de dependência cíclica entre classes (confissão):
(Time0): Duas classes, Sample e Well, se referem uma à outra (dependência cíclica). Well refere-se a Sample, e Sample refere-se novamente a Well, por conveniência (às vezes repetindo amostras, outras vezes repetindo todos os poços em uma placa). Eu não conseguia imaginar casos em que Sample não faria referência ao poço onde está colocado.
(Tempo1): Um ano depois, muitos casos de uso são implementados ... e agora existem casos em que a Amostra não deve se referir ao poço em que está colocado. Existem placas temporárias em uma etapa de trabalho. Aqui, um poço se refere a uma amostra, que por sua vez se refere a um poço em outra placa. Por esse motivo, às vezes ocorre um comportamento estranho quando alguém tenta implementar novos recursos. Leva tempo para penetrar.
Também fui ajudado por este artigo mencionado acima sobre aspectos negativos do carregamento lento.
fonte
No mundo ideal, o DDD propõe que as Entidades não devem ter referência às camadas de dados. mas não vivemos no mundo ideal. Os domínios podem precisar se referir a outros objetos de domínio para lógica de negócios com os quais eles podem não ter uma dependência. É lógico que as entidades se refiram à camada do repositório apenas para fins de leitura, para buscar os valores.
fonte