O isolamento do modelo de domínio / persistência geralmente é tão estranho?


12

Estou mergulhando nos conceitos do Domain-Driven Design (DDD) e achei alguns princípios estranhos, especialmente no que diz respeito ao isolamento do domínio e do modelo de persistência. Aqui está o meu entendimento básico:

  1. Um serviço na camada de aplicativo (fornecendo um conjunto de recursos) solicita objetos de domínio de um repositório necessário para executar sua função.
  2. A implementação concreta desse repositório busca dados do armazenamento para o qual foi implementado
  3. O serviço informa ao objeto de domínio, que encapsula a lógica de negócios, para executar determinadas tarefas que modificam seu estado.
  4. O serviço informa ao repositório para persistir o objeto de domínio modificado.
  5. O repositório precisa mapear o objeto de domínio de volta para a representação correspondente no armazenamento.

Ilustração de fluxo

Agora, dadas as suposições acima, o seguinte parece estranho:

Anúncio 2:

O modelo de domínio parece carregar o objeto de domínio inteiro (incluindo todos os campos e referências), mesmo que não sejam necessários para a função que o solicitou. Carregar completamente pode nem ser possível se outros objetos de domínio forem referenciados, a menos que você carregue também esses objetos de domínio e todos os objetos a que se referem, e assim por diante. O carregamento lento vem à mente, o que significa que você começa a consultar seus objetos de domínio, que devem ser de responsabilidade do repositório.

Dado esse problema, a maneira "correta" de carregar objetos do domínio parece ter uma função de carregamento dedicada para cada caso de uso. Essas funções dedicadas carregariam apenas os dados exigidos pelo caso de uso para o qual foram projetados. Aqui é onde o embaraço entra em jogo: primeiro, eu precisaria manter uma quantidade considerável de funções de carregamento para cada implementação do repositório, e os objetos de domínio acabariam em estados incompletos, carregando nullem seus campos. Tecnicamente, este último não deve ser um problema porque, se um valor não foi carregado, não deve ser exigido pela funcionalidade que o solicitou. Ainda é estranho e um risco potencial.

Anúncio 3:

Como um objeto de domínio verificaria restrições de exclusividade na construção, se ele não tem nenhuma noção do repositório? Por exemplo, se eu quisesse criar um novo Usercom um número de previdência social exclusivo (fornecido), o conflito mais antigo ocorreria ao solicitar ao repositório que salve o objeto, apenas se houver uma restrição de exclusividade definida no banco de dados. Caso contrário, eu poderia procurar um Usercom a previdência social fornecida e relatar um erro, caso exista, antes de criar uma nova. Mas as verificações de restrição permaneceriam no serviço e não no objeto de domínio ao qual pertencem. Acabei de perceber que os objetos de domínio estão muito bem autorizados a usar repositórios (injetados) para validação.

Anúncio 5:

Eu percebo o mapeamento de objetos de domínio para um back-end de armazenamento como um processo intensivo de trabalho, em comparação com os objetos de domínio modificando diretamente os dados subjacentes. É, obviamente, um pré-requisito essencial para dissociar a implementação concreta do armazenamento do código de domínio. No entanto, isso realmente tem um custo tão alto?

Aparentemente, você tem a opção de usar ferramentas ORM para fazer o mapeamento para você. No entanto, isso geralmente exige que você projete o modelo de domínio de acordo com as restrições do ORM ou introduza uma dependência do domínio para a camada de infraestrutura (usando anotações do ORM nos objetos de domínio, por exemplo). Também li que os ORMs introduzem uma sobrecarga computacional considerável.

No caso de bancos de dados NoSQL, para os quais quase não existem conceitos semelhantes a ORM, como você monitora quais propriedades foram alteradas nos modelos de domínio save()?

Editar : Além disso, para que um repositório acesse o estado do objeto de domínio (ou seja, o valor de cada campo), o objeto de domínio precisa revelar seu estado interno que quebra o encapsulamento.

Em geral:

  • Para onde iria a lógica transacional? Isso é certamente persistência específica. Algumas infra-estruturas de armazenamento podem nem mesmo suportar transações (como repositórios simulados na memória).
  • Para operações em massa que modificam vários objetos, eu precisaria carregar, modificar e armazenar cada objeto individualmente para passar pela lógica de validação encapsulada do objeto? Isso se opõe à execução de uma única consulta diretamente no banco de dados.

Gostaria de receber alguns esclarecimentos sobre este tópico. Minhas suposições estão corretas? Caso contrário, qual é a maneira correta de resolver esses problemas?


11
Bons pontos e perguntas, também estou interessado nisso. Uma observação lateral - se você estiver modelando adequadamente o agregado, o que significa que, em um determinado momento da existência, a instância agregada deve estar em um estado válido - esse é o ponto principal do agregado (e não o uso de um agregado como um contêiner de composição). Isso também significa que, para restaurar de forma agregada os dados do banco de dados, o próprio repositório normalmente teria que usar construtor específico e conjunto de operações de mutação, e não vejo como qualquer ORM poderia saber automaticamente como realizar essas operações automaticamente. .
Dusan

2
O que é ainda mais decepcionante é que essas perguntas como a sua estão perguntei ao redor muito frequentemente, mas, a meu conhecimento - há zero exemplos da aplicação dos agregados e repositórios que são pelo livro
Dusan

Respostas:


5

Seu entendimento básico está correto e a arquitetura que você esboça é boa e funciona bem.

Lendo nas entrelinhas, parece que você está vindo de um estilo de programação de registro ativo mais centrado no banco de dados? Para chegar a uma implementação funcional, eu diria que você precisa

1: Os objetos do domínio não precisam incluir o gráfico inteiro do objeto. Por exemplo, eu poderia ter:

public class Customer
{
    public string AddressId {get;set;}
    public string Name {get;set;}
}

public class Address
{
    public string Id {get;set;}
    public string HouseNumber {get;set;
}

Endereço e Cliente só precisam fazer parte da mesma agregação se você tiver alguma lógica, como "o nome do cliente só pode começar com a mesma letra que o nome da casa". Você tem razão para evitar o carregamento lento e as versões 'Lite' dos objetos.

2: As restrições de exclusividade geralmente são da competência do repositório e não do objeto do domínio. Não injete repositórios nos Objetos de Domínio, isso é um retorno ao registro ativo, simplesmente erro quando o serviço tenta salvar.

A regra de negócios não é "Não existem duas instâncias de Usuário com o mesmo SocialSecurityNumber ao mesmo tempo"

É que eles não podem existir no mesmo repositório.

3: Não é difícil escrever repositórios em vez de métodos de atualização de propriedades individuais. De fato, você descobrirá que possui praticamente o mesmo código de qualquer maneira. É exatamente em qual classe você a coloca.

Atualmente, os ORMs são fáceis e não têm restrições extras no seu código. Dito isto, pessoalmente, prefiro simplesmente pôr em marcha o SQL. Não é tão difícil, você nunca enfrenta problemas com os recursos do ORM e pode otimizar quando necessário.

Realmente não há necessidade de acompanhar quais propriedades foram alteradas quando você salvou. Mantenha seus Objetos de Domínio pequenos e simplesmente substitua a versão antiga.

Questões gerais

  1. A lógica de transação entra no repositório. Mas você não deve ter muito ou nada. Certamente, você precisará de algumas se tiver tabelas filho nas quais está colocando os objetos filhos do agregado, mas isso será totalmente encapsulado no método de repositório SaveMyObject.

  2. Atualizações em massa. Sim, você deve alterar individualmente cada objeto e adicionar um método SaveMyObjects (List objects) ao seu repositório, para fazer a atualização em massa.

    Você deseja que o objeto de domínio ou o serviço de domínio contenha a lógica. Não é o banco de dados. Isso significa que você não pode simplesmente "atualizar o nome do conjunto de clientes = x onde y", porque, apesar de tudo, você conhece o objeto Customer ou CustomerUpdateService faz 20 outras coisas estranhas ao alterar o nome.


Ótima resposta. Você está absolutamente certo, estou acostumado a um estilo de registro ativo de codificação, e é por isso que o padrão do repositório parece estranho à primeira vista. No entanto, os objetos de domínio "enxuto" (em AddressIdvez de Address) contradizem os princípios OO?
Double M

Não, você ainda tem um objeto de endereços, a sua não apenas uma criança de Cliente
Ewan

mapeamento de objeto de anúncio sem controle de alterações softwareengineering.stackexchange.com/questions/380274/…
Double M

2

Resposta curta: Seu entendimento está correto e as perguntas que você aponta para problemas válidos para os quais as soluções não são diretas nem universalmente aceitas.

Ponto 2 .: (carregando gráficos de objetos completos)

Não sou o primeiro a apontar que as ORMs nem sempre são uma boa solução. O principal problema é que os ORMs não sabem nada sobre o caso de uso real, portanto, não têm idéia do que carregar ou como otimizar. Isto é um problema.

Como você disse, a solução óbvia é ter métodos de persistência para cada caso de uso. Mas se você ainda usar um ORM para isso, o ORM forçará você a compactar tudo em objetos de dados. Que, além de não ser realmente orientado a objetos, novamente não é o melhor design para alguns casos de uso.

E se eu apenas quiser atualizar em massa alguns registros? Por que eu precisaria de uma representação de objeto para todos os registros? Etc.

Portanto, a solução para isso é simplesmente não usar um ORM para casos de uso para os quais não é um bom ajuste. Implemente um caso de uso "naturalmente" como ele é, que às vezes não requer uma "abstração" adicional dos dados em si (objetos de dados) nem uma abstração sobre as "tabelas" (repositórios).

Ter objetos de dados meio preenchidos ou substituir referências de objetos por "ids" são soluções alternativas, na melhor das hipóteses, não são bons projetos, como você apontou.

Ponto 3 .: (verificando restrições)

Se a persistência não for abstraída, cada caso de uso poderá obviamente verificar qualquer restrição que desejar facilmente. O requisito de que os objetos não conheçam o "repositório" é completamente artificial e não é um problema da tecnologia.

Ponto 5 .: (ORMs)

É, obviamente, um pré-requisito essencial para dissociar a implementação concreta do armazenamento do código de domínio. No entanto, isso realmente tem um custo tão alto?

Não, não faz. Existem muitas outras maneiras de ter persistência. O problema é que o ORM é visto como "a" solução a ser usada sempre (pelo menos para bancos de dados relacionais). Tentar sugerir não usá-lo para alguns casos de uso em um projeto é inútil e, dependendo do próprio ORM, às vezes até impossível, pois às vezes os caches e a execução tardia são usados ​​por essas ferramentas.

Pergunta geral 1: (transações)

Eu não acho que exista uma solução única. Se seu design for orientado a objetos, haverá um método "top" para cada caso de uso. A transação deve estar lá.

Qualquer outra restrição é completamente artificial.

Pergunta geral 2: (operações a granel)

Com um ORM, você (para a maioria dos ORMs que eu conheço) é forçado a passar por objetos individuais. Isso é completamente desnecessário e provavelmente não seria o seu design se sua mão não fosse atada pelo ORM.

O requisito para separar a "lógica" do SQL vem dos ORMs. Eles têm que dizer isso, porque não podem suportar. Não é inerentemente "ruim".

Sumário

Acho que meu argumento é que os ORMs nem sempre são a melhor ferramenta para um projeto e, mesmo que seja, é altamente improvável que seja o melhor para todos os casos de uso em um projeto.

Da mesma forma, a abstração do repositório de objeto de dados do DDD também nem sempre é a melhor. Eu diria até agora, eles raramente são o design ideal.

Isso não nos deixa com uma solução única, então teríamos que pensar em soluções para cada caso de uso individualmente, o que não é uma boa notícia e, obviamente, dificulta nosso trabalho :)


Pontos muito interessantes, obrigado por confirmar minhas suposições. Você disse que existem muitas outras maneiras de persistir. Você pode recomendar um padrão de design de desempenho a ser usado com bancos de dados de gráficos (sem ORM), que ainda forneceria PI?
Double M

11
Eu realmente questionaria se você precisa de isolamento (e de que tipo) em primeiro lugar. Isolar por tecnologia (ou seja, banco de dados, interface do usuário, etc.) traz quase automaticamente o "embaraço" que você está tentando evitar, para a vantagem de substituir um pouco mais facilmente a tecnologia de banco de dados. O custo é, no entanto, uma mudança mais difícil da lógica de negócios, uma vez que se espalha pelas camadas. Ou você pode dividir as funções de negócios, o que dificultaria a alteração dos bancos de dados, mas a lógica mais fácil. Qual você realmente quer?
Robert Bräutigam

11
Você pode obter o melhor desempenho se apenas modelar o domínio (ou seja, funções de negócios) e não abstrair o banco de dados (seja relacional ou gráfico, não importa). Como o banco de dados não é abstraído do caso de uso, ele pode implementar as consultas / atualizações ideais que deseja e não precisa passar por um modelo de objeto estranho para realizar o que deseja.
Robert Bräutigam

Bem, o objetivo principal é manter as preocupações de persistência afastadas da lógica de negócios, para ter um código limpo que seja fácil de entender, expandir e testar. Ser capaz de trocar tecnologias de banco de dados é apenas um bônus. Percebo que obviamente há atrito entre eficiência e ignorância, o que parece ser mais forte com os bancos de dados gráficos devido às consultas poderosas que você pode (mas não tem permissão para) usar.
Double M

11
Como desenvolvedor Java Enterprise, posso dizer que tentamos separar persistência da lógica nas últimas duas décadas. Isso não funciona. Primeiro, a separação nunca foi realmente alcançada. Até hoje, existem todos os tipos de coisas relacionadas ao banco de dados em objetos supostamente "comerciais", sendo o principal o ID do banco de dados (e muitas anotações do banco de dados). Segundo, como você disse, algumas vezes a lógica de negócios é executada no banco de dados de qualquer maneira. Terceiro, é por isso que temos bancos de dados específicos, para poder descarregar alguma lógica melhor executada onde estão os dados.
Robert Bräutigam
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.