DDD - a regra que as Entidades não podem acessar Repositórios diretamente

185

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.

codeulike
fonte
Dê uma olhada na minha pergunta stackoverflow.com/q/8269784/235715 , é uma situação em que é difícil capturar a lógica, sem que a Entity tenha acesso ao repositório. Embora eu ache que as entidades não devam ter acesso aos repositórios, e existe uma solução para minha situação em que o código pode ser reescrito sem referência ao repositório, mas atualmente não consigo pensar em nenhum.
Alex Burtsev #
Não sei de onde veio. Meus pensamentos: Eu acho que esse mal-entendido vem de pessoas que não entendem o que é DDD. Essa abordagem não é para implementar software, mas para projetá-lo (domínio .. design). Naquela época, tínhamos arquitetos e implementadores, mas agora existem apenas desenvolvedores de software. DDD é destinado a arquitetos. E quando um arquiteto está projetando software, ele precisa de alguma ferramenta ou padrão para representar uma memória ou banco de dados para desenvolvedores que implementarão o design preparado. Mas o próprio design (do ponto de vista comercial) não possui ou precisa de um repositório.
berhalak

Respostas:

47

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.

kertosis
fonte
15
Obrigado. Mas eu deveria estar me esforçando para obter o máximo de lógica de negócios possível nas entidades (e suas fábricas e especificações associadas e assim por diante), certo? Mas se nenhum deles tem permissão para buscar dados via Repositórios, como devo escrever qualquer lógica de negócios (razoavelmente complicada)? Por exemplo: O usuário do Chatroom não tem permissão para alterar seu nome para um nome que já tenha sido usado por outra pessoa. Gostaria que essa regra fosse incorporada pela entidade ChatUser, mas não é muito fácil de fazer se você não puder acessar o repositório a partir daí. Então, o que eu deveria fazer?
codeulike
Minha resposta foi maior do que a caixa de comentários permitiria, veja a edição.
precisa saber é
6
Sua entidade deve saber como se proteger de danos. Isso inclui garantir que ele não possa entrar em um estado inválido. O que você está descrevendo com o usuário da sala de bate-papo é a lógica comercial que reside, além da lógica que a entidade precisa para se manter válida. Lógica comercial como o que você realmente deseja em um serviço do Chatroom, não na entidade ChatUser.
Alec
9
Obrigado Alec. Essa é uma maneira clara de expressar isso. Mas, para mim, parece que a regra de ouro focada no domínio de Evans de "toda lógica de negócios deve entrar na camada de domínio" está em conflito com a regra de "entidades não devem acessar repositórios". Eu posso conviver com isso se entender o motivo, mas não consigo encontrar uma boa explicação on-line sobre por que as entidades não devem acessar repositórios. Evans não parece mencionar isso explicitamente. De onde veio? Se você pode postar uma resposta apontando para alguma boa literatura, você pode ser capaz de saco-se uma recompensa 50pt:)
codeulike
4
"o dele não faz sentido nas pequenas" Esse é um grande erro que as equipes cometem ... é um projeto pequeno, como tal, eu posso fazer isso e aquilo ... pare de pensar assim. Muitos dos pequenos projetos com os quais trabalhamos acabam se tornando grandes devido a requisitos de negócios. Se você fizer algo murchar pequeno ou grande, faça-o da maneira certa.
MeTitus
35

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:

  1. Deveríamos conhecer nossas intenções em uma solicitação e o que queremos do domínio, portanto, podemos fazer chamadas de repositório antes de construir ou invocar o comportamento agregado. Isso também ajuda a evitar o problema de estado inconsistente na memória e a necessidade de carregamento lento (consulte este artigo ). O cheiro é que você não pode mais criar uma instância na memória de sua entidade sem se preocupar com o acesso a dados.
  2. O CQS (Command Query Separation) pode ajudar a reduzir a necessidade de chamar o repositório para coisas em nossas entidades.
  3. Podemos usar uma especificação para encapsular e comunicar as necessidades da lógica do domínio e repassá-la ao repositório (um serviço pode orquestrar essas coisas para nós). A especificação pode vir da entidade encarregada de manter essa invariante. O repositório interpretará partes da especificação em sua própria implementação de consulta e aplicará regras da especificação nos resultados da consulta. Isso visa manter a lógica do domínio na camada de domínio. Também serve melhor a linguagem onipresente e a comunicação. Imagine dizer "especificação de pedido atrasado" em vez de "pedido de filtro de tbl_order onde colocado_at é menos de 30 minutos antes do sysdate" (consulte esta resposta ).
  4. Isso dificulta o raciocínio sobre o comportamento das entidades, uma vez que o Princípio de responsabilidade única é violado. Se você precisar resolver problemas de armazenamento / persistência, sabe para onde ir e para onde não ir.
  5. Evita o risco de conceder a uma entidade acesso bidirecional ao estado global (por meio dos serviços de repositório e domínio). Você também não deseja quebrar seu limite de transação.

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:

Como regra geral, devemos tentar evitar o uso de Repositórios (12) de dentro dos Agregados, se possível.

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):

A referência por identidade não impede completamente a navegação pelo modelo. Alguns usarão um Repositório (12) de dentro de um Agregado para pesquisa. Essa técnica é chamada Modelo de Domínio Desconectado e, na verdade, é uma forma de carregamento lento. Porém, há uma abordagem recomendada diferente: Use um Repositório ou Serviço de Domínio (7) para procurar objetos dependentes antes de invocar o comportamento Agregado. Um Serviço de Aplicativo do cliente pode controlar isso e, em seguida, despachar para o Agregado:

Ele mostra um exemplo disso no código:

public class ProductBacklogItemService ... { 

   ... 
   @Transactional 
   public void assignTeamMemberToTask( 
        String aTenantId, 
        String aBacklogItemId, 
        String aTaskId, 
        String aTeamMemberId) { 

        BacklogItem backlogItem = backlogItemRepository.backlogItemOfId( 
                                        new TenantId( aTenantId), 
                                        new BacklogItemId( aBacklogItemId)); 

        Team ofTeam = teamRepository.teamOfId( 
                                  backlogItem.tenantId(), 
                                  backlogItem.teamId());

        backlogItem.assignTeamMemberToTask( 
                  new TeamMemberId( aTeamMemberId), 
                  ofTeam,
                  new TaskId( aTaskId));
   } 
   ...
}     

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:

$user->mountFriends(); // <-- has a repository call inside that loads friends? 

2) Em um serviço de domínio, isso é bom:

public function mountYourFriends(MountFriendsCommand $mount) { /* see http://store.steampowered.com/app/296470/ */ 
    $user = $this->users->get($mount->userId()); 
    $friends = $this->users->findBySpecification($user->getFriendsSpecification()); 
    array_map([$user, 'mount'], $friends); 
}
programador
fonte
1
Pergunta: Sempre somos ensinados a não criar um objeto em um estado inválido ou inconsistente. Quando você carrega usuários do repositório e depois chama 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?
Jimbo
O repositório chama o domínio para criar uma instância nova. Você não obtém uma instância do usuário sem passar pelo domínio. O problema que esta resposta resolve é o contrário. Onde o domínio está referenciando o repositório, e isso deve ser evitado.
prograhammer
28

É 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 ....

Magnus Backeus
fonte
1
Bom ponto. O modelo de domínio da velha escola provavelmente teria a Entidade B responsável por validar a si mesma antes de se deixar persistir, eu acho. Você tem certeza de que Evans menciona entidades que não usam repositórios? Estou a meio do livro e ele não mencionou ainda ...
codeulike
Bem, eu li o livro há vários anos (bem 3 ...) e minha memória me falha. Não me lembro se ele a exprimiu exatamente, mas acredito que ele ilustrou isso através de exemplos. Você também pode encontrar uma interpretação comunitária do exemplo de Cargo (do livro) em dddsamplenet.codeplex.com . Faça o download do projeto de código (veja o projeto Vanilla - é o exemplo do livro). Você verá que os repositórios são usados ​​apenas na camada Aplicativo para acessar entidades do domínio.
amigos estão dizendo sobre magnus backeus
1
Ao fazer o download do exemplo DDD SmartCA do livro p2p.wrox.com/…, você verá outra abordagem (embora seja um cliente RIA para Windows) em que os repositórios são usados ​​nos serviços (nada de estranho aqui), mas os serviços são usados ​​por dentro. Isso é algo que eu não faria, mas sou um cara de aplicativo webb. Dado o cenário para o aplicativo SmartCA, no qual você deve poder trabalhar offline, talvez o design do ddd tenha uma aparência diferente.
amigos estão dizendo sobre magnus backeus
O exemplo do SmartCA parece interessante, em que capítulo está? (os downloads de código são organizados por capítulo)
codeulike
1
@ codeulike Atualmente, estou projetando e implementando uma estrutura usando conceitos de ddd. Às vezes, a validação precisa acessar o banco de dados e consultá-lo (exemplo: consulta para verificação de índice exclusivo de várias colunas). Com relação a isso e ao fato de que as consultas devem ser gravadas na camada do repositório, as entidades de domínio precisam ter referências a suas interfaces de repositório na camada de modelo de domínio para colocar a validação completamente na camada de modelo de domínio. Por fim, está tudo bem para entidades de domínio terem acesso a repositórios?
Karamafrooz 3/17/17
13

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.

  • Você deseja manter uma conexão estreita entre o modelo de domínio e o código
  • Separar preocupações técnicas ajuda a provar que o modelo é prático para implementação
  • Você deseja que a linguagem onipresente permeie o design do sistema

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:

var orderLines = OrderRepository.GetOrderLines(orderId);

foreach (var line in orderLines)
{
     total += line.Price;
}

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.

foreach (var line in order.OrderLines)
{
    total += line.Price;
}

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:

public class BillOrderFetchingStrategy : ILoadDataFor<IBillOrder, Order>
{
    Order Load(string aggregateId)
    {
        var order = new Order();

        order.Data = GetOrderLinesWithPrice(aggregateId);
    
        return order;
    }

}
   

Em seguida, seu agregado pode se parecer com:

public class Order : IBillOrder
{
    void BillOrder(BillOrderCommand command)
    {
        foreach (var line in this.Data.OrderLines)
        {
            total += line.Price;
        }

        etc...
    }
}

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.

ttg
fonte
Obrigado, vou verificar os links. Mas na sua resposta você está confundindo "separação de preocupações" com "nenhum acesso a ela"? Certamente a maioria das pessoas concorda que a camada de persistência deve ser mantida separada da camada em que as Entidades estão. Mas isso é diferente de dizer 'as entidades não devem nem conseguir ver a camada de persistência, mesmo por meio de um agnóstico de implementação muito geral interface'.
precisa saber é o seguinte
Carregando dados por meio de uma interface ou não, você ainda está preocupado em carregar dados ao implementar regras de negócios. Concordo que muitas pessoas ainda chamam essa separação de preocupações, talvez um princípio de responsabilidade única teria sido um termo melhor para usar.
ttg
1
Não tenho muita certeza de como analisar seu último comentário, mas acho que você está sugerindo que os dados não sejam carregados durante o processamento das regras de negócios? Vejo que tornaria as regras "mais puras". Mas muitos tipos de regras de negócios precisarão se referir a outros dados - você está sugerindo que eles sejam carregados antecipadamente por um objeto separado?
codeulike
@ codeulike: Eu atualizei minha resposta. Você ainda pode carregar dados durante as regras de negócios, se achar que precisa, mas isso não exige a adição de linhas de código de acesso a dados no modelo de domínio (por exemplo, carregamento lento). Nos modelos de domínio que eu projetei, os dados geralmente são carregados com antecedência, como você disse. Descobri que a execução de regras de negócios geralmente não requer uma quantidade excessiva de dados.
ttg
12

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).

// Entity
public class Invoice
{
    ...
    public void SetStatus(StatusCode statusCode, DateTime dateTime) { ... }
    public void CreateCreditNote(decimal amount) { ... }
    ...
}

Queremos conseguir isso sem injetar nenhum serviço no construtor de uma entidade, porque:

  • A introdução de um novo comportamento (que usa um novo serviço) pode levar a uma mudança no construtor, significando que a mudança afeta todas as linhas que instanciam a entidade !
  • Esses serviços não fazem parte do modelo , mas a injeção de construtores sugeriria que sim.
  • Geralmente, um serviço (até sua interface) é um detalhe de implementação, e não parte do domínio. O modelo de domínio teria uma dependência voltada para o exterior .
  • Pode ser confuso por que a entidade não pode existir sem essas dependências. (Um serviço de nota de crédito, você diz? Eu nem vou fazer nada com notas de crédito ...)
  • Seria difícil instanciar, portanto, difícil de testar .
  • O problema se espalha facilmente, porque outras entidades que contêm esse teriam as mesmas dependências - que nelas podem parecer dependências muito não naturais .

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.

public class Invoice
{
    ...

    // Simple method injection
    public void SetStatus(IInvoiceLogger logger, StatusCode statusCode, DateTime dateTime)
    { ... }

    // Double dispatch
    public void CreateCreditNote(ICreditNoteService creditNoteService, decimal amount)
    {
        creditNoteService.CreateCreditNote(this, amount);
    }

    ...
}

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 da Invoiceentidade.

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 de IInvoiceServiceajuda 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 por SetStatus()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) e IInvoiceServiceimplementamos essa interface. O código do cliente pode simplesmente usar seu único IInvoiceServicepara todos os Invoicemé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.

public class Invoice
{
    public IEnumerable<CreditNote> GetCreditNotes(ICreditNoteRepository repository)
    { ... }
}

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.

public class Invoice
{
    // Lazy could use an interface (for contravariance if nothing else), but I digress
    public Lazy<IEnumerable<CreditNote>> CreditNotes { get; }

    // Give me something that will provide my credit notes
    public Invoice(Func<Invoice, IEnumerable<CreditNote>> lazyCreditNotes)
    {
        this.CreditNotes = new Lazy<IEnumerable<CreditNotes>>() => lazyCreditNotes(this));
    }
}

Por um lado, um repositório que carrega um Invoicedo banco de dados pode ter acesso livre a uma função que carrega as notas de crédito correspondentes e injeta essa função no Invoice.

Por outro lado, o código que cria um novo real Invoicepassa apenas uma função que retorna uma lista vazia:

new Invoice(inv => new List<CreditNote>() as IEnumerable<CreditNote>)

(Um costume ILazy<out T>poderia nos livrar do feio elenco IEnumerable, mas isso complicaria a discussão.)

// Or just an empty IEnumerable
new Invoice(inv => IEnumerable.Empty<CreditNote>())

Ficaria feliz em ouvir suas opiniões, preferências e melhorias!

Timo
fonte
3

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:

  • Separação de preocupações (as entidades devem ser separadas da maneira como são persistidas. Pois pode haver várias estratégias nas quais a mesma entidade seria mantida, dependendo do cenário de uso)
  • Logicamente, as entidades podem ser vistas em um nível abaixo do nível em que os repositórios operam. Os componentes de nível inferior não devem ter conhecimento dos componentes de nível superior. Portanto, as entradas não devem ter conhecimento de repositórios.
user1502505
fonte
2

simplesmente Vernon Vaughn fornece uma solução:

Use um serviço de repositório ou domínio para procurar objetos dependentes antes de chamar o comportamento agregado. Um serviço de aplicativo cliente pode controlar isso.

Alireza Rahmani Khalili
fonte
Mas não de uma entidade.
ssmith
De Vernon Vaughn IDDD fonte: public class Calendar estende EventSourcedRootEntity {... public CalendarEntry scheduleCalendarEntry (CalendarIdentityService aCalendarIdentityService,
Teimuraz
Confira seu artigo @Teimuraz
Alireza Rahmani Khalili
1

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 Repositorycamada (também conhecida como Data Accesscamada) 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 ;-)

umlcat
fonte
1
Obrigado, mas não estou perguntando sobre separar o acesso a dados da lógica de negócios - isso é uma coisa muito clara com a qual existe um amplo acordo. Estou perguntando por que, em arquiteturas DDD como S # arp, as Entidades não têm permissão para 'conversar' com a camada de acesso a dados. É um arranjo interessante sobre o qual não consegui encontrar muita discussão.
codeulike
0

Isso veio do livro de Eric Evans Domain Driven Design, ou veio de outro lugar?

É coisa velha. O livro de Eric apenas fez vibrar um pouco mais.

Onde existem boas explicações para o raciocínio por trás disso?

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.

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)

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.

Arnis Lapsa
fonte
Certo. Então, como uma Entidade totalmente persistente e ignorante implementa a Business Logic se ela nem sequer tem permissão para falar com a camada de persistência? O que ele faz quando precisa examinar valores em outras entidades arbitrárias?
codeulike
Se sua entidade precisar examinar valores em outras entidades arbitrárias, você provavelmente terá alguns problemas de design. Talvez considere dividir as aulas para que sejam mais coesas.
Cdaq
0

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.

Edvard Englund
fonte
-1

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.

vsingh
fonte
Não, isso introduz acoplamentos desnecessários às entidades, viola o SRP e a separação de interesses e dificulta a desserialização da entidade da persistência (já que o processo de desserialização agora também deve injetar serviços / repositórios que a entidade frequenta).
ssmith