Modelo de domínio rico versus anêmico [fechado]

93

Estou decidindo se devo usar um Modelo de Domínio Rico em vez de um Modelo de Domínio Anêmico e estou procurando bons exemplos dos dois.

Tenho construído aplicações web usando um Modelo de Domínio Anêmico, apoiado por um Serviço -> Repositório -> Sistema de camada de armazenamento , usando FluentValidation para validação de BL, e colocando todo meu BL na camada de Serviço.

Eu li o livro DDD de Eric Evan, e ele (junto com Fowler e outros) parece pensar que Modelos de Domínio Anêmico são um antipadrão.

Então, eu só queria saber mais sobre esse problema.

Além disso, estou realmente procurando alguns bons exemplos (básicos) de um Modelo de Domínio Rico e os benefícios sobre o Modelo de Domínio Anêmico que ele oferece.

Sam
fonte
Você também pode querer verificar este blog que argumenta a favor do modelo de domínio anêmico
Japheth Ongeri - inkalimeva
14
DDD> ADM , ADM> DDD , DDD> ADM , ADM> DDD , ADM + DDD ... DDD / ADM, ou como não concordar sobre design de software !
sp00m de
Aqui está um exemplo de como evitar o modelo de domínio anêmico: medium.com/@wrong.about/…
Vadim Samokhin 01 de
11
É engraçado que essa pergunta pudesse ter sido respondida com um único link para um projeto do mundo real financiado por uma organização real. Após 5 anos, nenhuma boa resposta, IMO. Falar é fácil. Mostre-me o código.
Mateusz Stefek

Respostas:

57

A diferença é que um modelo anêmico separa a lógica dos dados. A lógica é muitas vezes colocados em classes nomeadas **Service, **Util, **Manager, **Helpere assim por diante. Essas classes implementam a lógica de interpretação de dados e, portanto, usam o modelo de dados como argumento. Por exemplo

public BigDecimal calculateTotal(Order order){
...
}

enquanto a abordagem de domínio rico inverte isso, colocando a lógica de interpretação de dados no modelo de domínio rico. Assim, ele reúne lógica e dados e um modelo de domínio rico ficaria assim:

order.getTotal();

Isso tem um grande impacto na consistência do objeto. Visto que a lógica de interpretação de dados envolve os dados (os dados só podem ser acessados ​​por meio de métodos de objeto), os métodos podem reagir às mudanças de estado de outros dados -> Isso é o que chamamos de comportamento.

Em um modelo anêmico, os modelos de dados não podem garantir que estejam em um estado legal, enquanto em um modelo de domínio rico podem. Um modelo de domínio rico aplica princípios OO como encapsulamento, ocultação de informações e junção de dados e lógica e, portanto, um modelo anêmico é um antipadrão de uma perspectiva OO.

Para uma visão mais aprofundada, dê uma olhada em meu blog https://www.link-intersystems.com/blog/2011/10/01/anemic-vs-rich-domain-models/

René Link
fonte
15
Digamos que o cálculo do preço total de um pedido envolva: 1) Aplicar um desconto que depende do cliente ser membro de um dos vários programas de fidelidade possíveis. 2) Aplicar um desconto para pedidos que contenham um grupo específico de itens juntos, dependendo da campanha de marketing atual executada pela loja. 3) Cálculo do imposto onde o valor do imposto depende de cada item específico do pedido. Em sua opinião, onde caberia toda essa lógica? Você poderia dar um exemplo simples de pseudo-código. Obrigado!
Nik
4
@Nik No modelo avançado, o Pedido teria uma referência ao objeto Cliente e o objeto Cliente teria uma referência ao Programa de Fidelidade. Assim, o Pedido teria acesso a todas as informações de que precisava, sem precisar de referências explícitas a coisas como serviços e repositórios dos quais buscar essas informações. No entanto, parece fácil encontrar um caso em que referências cíclicas estão acontecendo. Ou seja, o pedido faz referência ao cliente, o cliente tem uma lista de todos os pedidos. Acho que pode ser parcialmente por isso que as pessoas preferem o Anemic agora.
esmagamento em
3
@crush A abordagem que você descreve funciona muito bem. Existe um problema. Provavelmente armazenamos as entidades em um banco de dados. Portanto, para calcular o total de um pedido, temos que buscar no banco de dados Pedido, Cliente, Programa de Fidelidade, Campanha de Marketing, Tabela de Impostos. Considere também que um Cliente possui uma coleção de Pedidos, um Programa de Fidelidade possui uma coleção de Clientes e assim por diante. Se buscarmos ingenuamente tudo isso, acabaremos carregando todo o banco de dados na RAM. Isso não é viável, é claro, então recorremos ao carregamento apenas de dados relevantes do banco de dados ... 1/2
Nik
3
@Nik "Se buscarmos nativamente todos eles, acabaremos carregando todo o banco de dados na RAM." Essa é uma das principais desvantagens do modelo rico em minha mente também. O modelo avançado é bom até que seu domínio se torne grande e complexo e, então, você comece a atingir as limitações de infraestrutura. É aí que os ORMs de carregamento lento podem ajudar, no entanto. Encontre um bom e você poderá reter modelos ricos enquanto não carrega o banco de dados inteiro na memória quando você só precisa de 1/20 dele. Dito isso, eu mesmo tendo a usar o Modelo Anêmico com CQRS depois de muitos anos indo e voltando entre anêmicos e ricos.
esmagamento de
2
Outra coisa a se considerar é onde reside sua lógica de domínio de negócios. Mais e mais desenvolvedores estão mudando-o do banco de dados e para os aplicativos aos quais ele pertence, em minha opinião. Mas se você estiver preso em uma situação em que sua empresa exige que a lógica de negócios permaneça na camada de banco de dados (procedimentos armazenados), então quase certamente não se beneficiará com a adição dessa lógica em um modelo de domínio rico. Na verdade, você poderia apenas estar se preparando para encontrar conflitos em que os procedimentos armazenados têm regras diferentes da camada de domínio do seu aplicativo ...
esmagar
53

Bozhidar Bozhanov parece argumentar a favor do modelo anêmico nesta postagem do blog.

Aqui está o resumo que ele apresenta:

  • objetos de domínio não devem ser gerenciados por Spring (IoC), eles não devem ter DAOs ou qualquer coisa relacionada à infraestrutura injetada neles

  • objetos de domínio têm os objetos de domínio dos quais dependem definidos pelo hibernate (ou mecanismo de persistência)

  • objetos de domínio executam a lógica de negócios, como é a ideia central do DDD, mas isso não inclui consultas de banco de dados ou operações CRUD - apenas no estado interno do objeto

  • raramente há necessidade de DTOs - os objetos de domínio são os próprios DTOs na maioria dos casos (o que salva algum código clichê)

  • os serviços realizam operações CRUD, enviam e-mails, coordenam os objetos do domínio, geram relatórios com base em vários objetos do domínio, executam consultas, etc.

  • a camada de serviço (aplicativo) não é tão fina, mas não inclui regras de negócios que são intrínsecas aos objetos de domínio

  • a geração de código deve ser evitada. Abstração, padrões de projeto e DI devem ser usados ​​para superar a necessidade de geração de código e, em última análise, para se livrar da duplicação de código.

ATUALIZAR

Recentemente li este artigo em que o autor defende seguir uma espécie de abordagem híbrida - objetos de domínio podem responder a várias perguntas com base apenas em seu estado (o que, no caso de modelos totalmente anêmicos, provavelmente seria feito na camada de serviço)

geoand
fonte
11
Não posso extrair desse artigo que Bozho parece argumentar a favor do modelo de domínio anêmico. a camada de serviço (aplicativo) não é tão fina, mas não inclui regras de negócios que são intrínsecas aos objetos de domínio . O que eu entendo é que os objetos de domínio devem conter a lógica de negócios intrínseca a eles, mas não devem conter nenhuma outra lógica de infraestrutura . Essa abordagem não me parece de forma alguma um modelo de domínio anêmico.
Utku
8
Também este: objetos de domínio executam a lógica de negócios, como é a ideia central do DDD, mas isso não inclui consultas de banco de dados ou operações apenas CRUD no estado interno do objeto . Essas afirmações não parecem favorecer de forma alguma o modelo de domínio anêmico. Eles apenas afirmam que a lógica da infraestrutura não deve ser acoplada a objetos de domínio. Pelo menos é o que eu entendo.
Utku
@Utku Na minha opinião, parece bastante claro que Bozho defende uma espécie de híbrido entre os dois modelos, um híbrido que eu diria que está mais próximo do modelo anêmico do que do modelo rico.
geoand
41

Meu ponto de vista é este:

Modelo de domínio anêmico = tabelas de banco de dados mapeadas para objetos (apenas valores de campo, nenhum comportamento real)

Modelo de domínio rico = uma coleção de objetos que expõe o comportamento

Se você deseja criar um aplicativo CRUD simples, talvez um modelo anêmico com um framework MVC clássico seja o suficiente. Mas se você deseja implementar algum tipo de lógica, o modelo anêmico significa que você não fará programação orientada a objetos.

* Observe que o comportamento do objeto não tem nada a ver com persistência. Uma camada diferente (mapeadores de dados, repositórios etc.) é responsável por persistir os objetos de domínio.

George
fonte
5
Desculpe pela minha ignorância, mas como um modelo de domínio rico pode seguir o princípio SOLID se você colocar toda a lógica relacionada à Entidade na classe. Isso viola o princípio SOLID, exatamente o 'S', que significa responsabilidade única, que diz que uma classe deve fazer apenas uma coisa e fazê-la direito.
redigaffi
5
@redigaffi Depende de como você define "uma coisa". Considere uma classe com duas propriedades e dois métodos: x, y, sume difference. São quatro coisas. Ou você pode argumentar que é adição e subtração (duas coisas). Ou você pode argumentar que é matemática (uma coisa). Existem muitos posts por aí sobre como encontrar um equilíbrio na aplicação do SRP. Aqui está um: hackernoon.com/…
Rainbolt
2
No DDD, a responsabilidade única significa que uma classe / modelo pode gerenciar seu próprio estado sem causar nenhum efeito colateral no resto do sistema como um todo. Qualquer outra definição simplesmente resulta em tediosos debates filosóficos em minha experiência.
ZombieTfk
12

Em primeiro lugar, copiei colei a resposta deste artigo http://msdn.microsoft.com/en-gb/magazine/dn385704.aspx

A Figura 1 mostra um Modelo de Domínio Anêmico, que é basicamente um esquema com getters e setters.

Figure 1 Typical Anemic Domain Model Classes Look Like Database Tables

public class Customer : Person
{
  public Customer()
  {
    Orders = new List<Order>();
  }
  public ICollection<Order> Orders { get; set; }
  public string SalesPersonId { get; set; }
  public ShippingAddress ShippingAddress { get; set; }
}
public abstract class Person
{
  public int Id { get; set; }
  public string Title { get; set; }
  public string FirstName { get; set; }
  public string LastName { get; set; }
  public string CompanyName { get; set; }
  public string EmailAddress { get; set; }
  public string Phone { get; set; }
}

Nesse modelo mais rico, em vez de simplesmente expor propriedades para serem lidas e gravadas, a superfície pública do Cliente é composta de métodos explícitos.

Figure 2 A Customer Type That’s a Rich Domain Model, Not Simply Properties

public class Customer : Contact
{
  public Customer(string firstName, string lastName, string email)
  {
    FullName = new FullName(firstName, lastName);
    EmailAddress = email;
    Status = CustomerStatus.Silver;
  }
  internal Customer()
  {
  }
  public void UseBillingAddressForShippingAddress()
  {
    ShippingAddress = new Address(
      BillingAddress.Street1, BillingAddress.Street2,
      BillingAddress.City, BillingAddress.Region,
      BillingAddress.Country, BillingAddress.PostalCode);
  }
  public void CreateNewShippingAddress(string street1, string street2,
   string city, string region, string country, string postalCode)
  {
    ShippingAddress = new Address(
      street1,street2,
      city,region,
      country,postalCode)
  }
  public void CreateBillingInformation(string street1,string street2,
   string city,string region,string country, string postalCode,
   string creditcardNumber, string bankName)
  {
    BillingAddress = new Address      (street1,street2, city,region,country,postalCode );
    CreditCard = new CustomerCreditCard (bankName, creditcardNumber );
  }
  public void SetCustomerContactDetails
   (string email, string phone, string companyName)
  {
    EmailAddress = email;
    Phone = phone;
    CompanyName = companyName;
  }
  public string SalesPersonId { get; private set; }
  public CustomerStatus Status { get; private set; }
  public Address ShippingAddress { get; private set; }
  public Address BillingAddress { get; private set; }
  public CustomerCreditCard CreditCard { get; private set; }
}
Razan Paul
fonte
2
Há um problema com os métodos que criam um objeto e atribuem uma propriedade ao objeto recém-criado. Eles tornam o código menos extensível e flexível. 1) E se o consumidor de código não quiser criar Address, mas ExtendedAddressherdar de Address, várias propriedades adicionais? 2) Ou altere CustomerCreditCardos parâmetros do construtor para assumir em BankIDvez de BankName?
Lightman
O que é criar um endereço requer serviços adicionais do que o que compõe o objeto? Você fica com a injeção de método para obter esses serviços. E se forem muitos serviços?
esmagamento de
8

Um dos benefícios das classes de domínio ricas é que você pode chamar seu comportamento (métodos) sempre que tiver a referência ao objeto em qualquer camada. Além disso, você tende a escrever métodos pequenos e distribuídos que colaboram juntos. Em classes de domínio anêmicas, você tende a escrever métodos processuais gordos (na camada de serviço) que geralmente são orientados pelo caso de uso. Eles geralmente são menos fáceis de manter em comparação com as classes de domínio avançadas.

Um exemplo de classes de domínio com comportamentos:

class Order {

     String number

     List<OrderItem> items

     ItemList bonus

     Delivery delivery

     void addItem(Item item) { // add bonus if necessary }

     ItemList needToDeliver() { // items + bonus }

     void deliver() {
         delivery = new Delivery()
         delivery.items = needToDeliver()
     }

}

O método needToDeliver()retornará uma lista de itens que precisam ser entregues, incluindo bônus. Ele pode ser chamado dentro da classe, de outra classe relacionada ou de outra camada. Por exemplo, se você passar Orderpara visualizar, poderá usar needToDeliver()de selecionado Orderpara exibir a lista de itens a serem confirmados pelo usuário antes de clicar no botão Salvar para persistir o Order.

Respondendo a um comentário

É assim que eu uso a classe de domínio do controlador:

def save = {
   Order order = new Order()
   order.addItem(new Item())
   order.addItem(new Item())
   repository.create(order)
}

A criação de Ordere LineItemé em uma transação. Se um dos LineItemnão puder ser criado, nenhum Orderserá criado.

Costumo ter métodos que representam uma única transação, como:

def deliver = {
   Order order = repository.findOrderByNumber('ORDER-1')
   order.deliver()       
   // save order if necessary
}

Qualquer coisa dentro deliver()será executada como uma única transação. Se eu precisar executar muitos métodos não relacionados em uma única transação, criaria uma classe de serviço.

Para evitar a exceção de carregamento lento, eu uso o gráfico de entidade nomeada JPA 2.1. Por exemplo, no controlador para tela de entrega, posso criar um método para carregar o deliveryatributo e ignorar bonus, como repository.findOrderByNumberFetchDelivery(). Na tela de bônus, chamo outro método que carrega o bonusatributo e ignoro delivery, como repository.findOrderByNumberFetchBonus(). Isso requer disciplina, pois ainda não consigo ligar deliver()para a tela de bônus.

Jocki
fonte
1
E quanto ao escopo da transação?
kboom
5
Os comportamentos do modelo de domínio não devem conter lógica de persistência (incluindo transação). Eles devem ser testáveis ​​(em teste de unidade) sem serem conectados ao banco de dados. O escopo da transação é responsabilidade da camada de serviço ou da camada de persistência.
jocki
1
Que tal carregamento lento então?
kboom
Quando você cria instâncias de classes de domínio no teste de unidade, elas não estão no estado gerenciado porque são objetos simples. Todos os comportamentos podem ser testados adequadamente.
jocki
E o que acontece quando você espera o objeto de domínio da camada de serviço? Não é administrado então?
kboom
8

Quando eu costumava escrever aplicativos de desktop monolíticos, construí modelos de domínio ricos, costumava gostar de criá-los.

Agora eu escrevo microsserviços HTTP minúsculos, há o mínimo de código possível, incluindo DTOs anêmicos.

Acho que o DDD e esse argumento anêmico datam da era monolítica de desktop ou aplicativo de servidor. Lembro-me dessa época e concordo que os modelos anêmicos são estranhos. Eu construí um grande aplicativo de negociação FX monolítico e não havia nenhum modelo, realmente, era horrível.

Com microsserviços, os pequenos serviços com seu rico comportamento, são indiscutivelmente os modelos combináveis ​​e agregados dentro de um domínio. Portanto, as próprias implementações de microsserviço podem não exigir mais DDD. O aplicativo de microsserviço pode ser o domínio.

Um microsserviço de pedidos pode ter muito poucas funções, expressas como recursos RESTful ou via SOAP ou qualquer outra coisa. O código do microsserviço de pedidos pode ser extremamente simples.

Um serviço único (micro) único mais monolítico maior, especialmente aquele que o mantém na memória RAM, pode se beneficiar do DDD.

Luke Puplett
fonte
Você tem algum exemplo de código para microsserviços HTTP que represente o seu estado da arte atual? Não pedindo que você escreva nada, apenas compartilhe links se tiver algo que possa apontar. Obrigado.
Casey Plummer
3

Acho que a raiz do problema está na falsa dicotomia. Como é possível extrair esses 2 modelos: ricos e "anêmicos" e contrastá-los? Acho que só é possível se você tiver ideias erradas sobre o que é uma classe . Não tenho certeza, mas acho que encontrei em um dos vídeos de Bozhidar Bozhanov no Youtube. Uma classe não é um dados + métodos sobre esses dados. É um entendimento totalmente inválido que leva à divisão das classes em duas categorias: somente dados, modelo tão anêmico e dados + métodos - modelo tão rico (para ser mais correto, há uma 3ª categoria: métodos apenas pares).

A verdade é que classe é um conceito em algum modelo ontológico, uma palavra, uma definição, um termo, uma ideia, é um DENOTAT . E esse entendimento elimina a falsa dicotomia: você não pode ter APENAS modelo anêmico ou APENAS modelo rico, pois significa que seu modelo não é adequado, não é relevante para a realidade: alguns conceitos possuem apenas dados, alguns deles possuem apenas métodos, alguns deles são misturados. Porque tentamos descrever, neste caso, algumas categorias, conjuntos de objetos, relações, conceitos com classes, e como sabemos, alguns conceitos são apenas processos (métodos), alguns deles são apenas conjuntos de atributos (dados), alguns de eles são relações com atributos (mistos).

Acho que uma aplicação adequada deve incluir todos os tipos de classes e evitar que se autolimite fanaticamente a apenas um modelo. Não importa como a lógica está representando: com código ou com objetos de dados interpretáveis ​​(como Mônadas Livres ), de qualquer maneira: devemos ter classes (conceitos, denotats) representando processos, lógica, relações, atributos, recursos, dados, etc. e não tentar evitar alguns deles ou reduzir todos eles a um único tipo.

Assim, podemos extrair lógica para outra classe e deixar os dados na classe original, mas não faz sentido porque alguns conceitos podem incluir atributos e relações / processos / métodos e uma separação deles duplicará o conceito sob 2 nomes que podem ser reduzido a padrões: "OBJECT-Attributes" e "OBJECT-Logic". É bom em linguagens procedurais e funcionais por causa de suas limitações, mas é autocontenção excessiva para uma linguagem que permite a você descrever todos os tipos de conceitos.

Paul-AG
fonte
1

Modelos de domínio anêmico são importantes para ORM e fácil transferência através de redes (o sangue vital de todas as aplicações comerciais), mas OO é muito importante para encapsular e simplificar as partes 'transacionais / manuseio' de seu código.

Portanto, o importante é ser capaz de se identificar e converter de um mundo para o outro.

Nomeie modelos anêmicos como AnemicUser ou UserDAO etc. para que os desenvolvedores saibam que há uma classe melhor para usar e, em seguida, tenham um construtor apropriado para a classe não anêmica

User(AnemicUser au)

e o método do adaptador para criar a classe anêmica para transporte / persistência

User::ToAnemicUser() 

Objetivo de usar o usuário não anêmico em qualquer lugar fora do transporte / persistência

andrew pate
fonte
-1

Aqui está um exemplo que pode ajudar:

Anêmico

class Box
{
    public int Height { get; set; }
    public int Width { get; set; }
}

Não anêmico

class Box
{
    public int Height { get; private set; }
    public int Width { get; private set; }

    public Box(int height, int width)
    {
        if (height <= 0) {
            throw new ArgumentOutOfRangeException(nameof(height));
        }
        if (width <= 0) {
            throw new ArgumentOutOfRangeException(nameof(width));
        }
        Height = height;
        Width = width;
    }

    public int area()
    {
       return Height * Width;
    }
}
Alireza Rahmani Khalili
fonte
Parece que ele pode ser convertido em um ValueObject versus uma Entidade.
código 5
Apenas copiar e colar da Wikipedia sem qualquer explicação
primeiro dia
quem escreveu antes? @wst
Alireza Rahmani Khalili
@AlirezaRahmaniKhalili de acordo com a história da Wikipedia, eles foram os primeiros ... A menos que eu não tenha entendido sua pergunta.
wst
-1

A abordagem clássica para DDD não afirma evitar modelos anêmicos versus modelos ricos a todo custo. No entanto, o MDA ainda pode aplicar todos os conceitos DDD (contextos limitados, mapas de contexto, objetos de valor etc.), mas usar modelos anêmicos versus ricos em todos os casos. Há muitos casos em que o uso de Serviços de Domínio para orquestrar Casos de Uso de Domínio complexos em um conjunto de agregados de domínio é uma abordagem muito melhor do que apenas agregados sendo chamados da camada de aplicativo. A única diferença da abordagem DDD clássica é onde todas as validações e regras de negócios residem? Há uma nova construção conhecida como validadores de modelo. Os validadores garantem a integridade de todo o modelo de entrada antes que qualquer caso de uso ou fluxo de trabalho de domínio ocorra. A raiz agregada e as entidades filhas são anêmicas, mas cada uma pode ter seus próprios validadores de modelo chamados conforme necessário, por seu validador de raiz. Os validadores ainda aderem ao SRP, são fáceis de manter e podem ser testados em unidades.

O motivo dessa mudança é que agora estamos avançando mais em direção a uma API primeiro em vez de uma abordagem UX inicial para microsserviços. REST desempenhou um papel muito importante nisso. A abordagem tradicional da API (por causa do SOAP) foi inicialmente fixada em uma API baseada em comandos versus verbos HTTP (POST, PUT, PATCH, GET e DELETE). Uma API baseada em comandos se encaixa bem com a abordagem orientada a objetos Rich Model e ainda é muito válida. No entanto, APIs simples baseadas em CRUD, embora possam caber em um Modelo Rico, são muito mais adequadas com modelos anêmicos simples, validadores e Serviços de Domínio para orquestrar o resto.

Eu amo o DDD em tudo o que ele tem a oferecer, mas chega um momento em que você precisa esticá-lo um pouco para se adequar a mudanças constantes e uma abordagem melhor à arquitetura.

código 5
fonte