Modelo de dominio rico frente a anémico [cerrado]

99

Estoy decidiendo si debo usar un modelo de dominio rico en lugar de un modelo de dominio anémico, y estoy buscando buenos ejemplos de los dos.

He estado construyendo aplicaciones web usando un modelo de dominio anémico, respaldado por un servicio -> repositorio -> sistema de capa de almacenamiento , usando FluentValidation para la validación de BL y poniendo todo mi BL en la capa de servicio.

He leído el libro DDD de Eric Evan, y él (junto con Fowler y otros) parece pensar que los modelos de dominio anémico son un anti-patrón.

Así que realmente quería tener una idea de este problema.

Además, realmente estoy buscando algunos buenos ejemplos (básicos) de un modelo de dominio enriquecido y los beneficios que ofrece sobre el modelo de dominio anémico.

Sam
fuente
Es posible que también desee consultar este blog que argumenta a favor del modelo de dominio anémico
Japheth Ongeri - inkalimeva
14
DDD> ADM , ADM> DDD , DDD> ADM , ADM> DDD , ADM + DDD ... DDD / ADM, ¡o cómo no estar de acuerdo con el diseño de software !
sp00m
Aquí hay un ejemplo de cómo evitar el modelo de dominio anémico: medium.com/@wrong.about/…
Vadim Samokhin
11
Es curioso que esta pregunta se haya podido responder con un solo enlace a un proyecto del mundo real financiado por una organización real. Después de 5 años, no hay una buena respuesta, en mi opinión. Hablar es barato. Enséñame el código.
Mateusz Stefek

Respuestas:

59

La diferencia es que un modelo anémico separa la lógica de los datos. La lógica se colocan a menudo en las clases nombradas **Service, **Util, **Manager, **Helpery así sucesivamente. Estas clases implementan la lógica de interpretación de datos y, por lo tanto, toman el modelo de datos como argumento. P.ej

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

mientras que el enfoque de dominio rico invierte esto al colocar la lógica de interpretación de datos en el modelo de dominio rico. Por lo tanto, junta la lógica y los datos y un modelo de dominio rico se vería así:

order.getTotal();

Esto tiene un gran impacto en la coherencia del objeto. Dado que la lógica de interpretación de datos envuelve los datos (solo se puede acceder a los datos a través de métodos de objeto), los métodos pueden reaccionar a los cambios de estado de otros datos -> Esto es lo que llamamos comportamiento.

En un modelo anémico, los modelos de datos no pueden garantizar que estén en un estado legal, mientras que en un modelo de dominio rico sí pueden. Un modelo de dominio rico aplica principios de OO como encapsulación, ocultación de información y unir datos y lógica y, por lo tanto, un modelo anémico es un anti patrón desde la perspectiva de OO.

Para una visión más profunda, eche un vistazo a mi blog https://www.link-intersystems.com/blog/2011/10/01/anemic-vs-rich-domain-models/

René Link
fuente
15
Digamos que calcular el precio total de un pedido implica: 1) Aplicar un descuento que depende de que el cliente sea miembro de uno de los muchos programas de fidelización posibles. 2) Aplicar un descuento para pedidos que contengan un grupo específico de artículos juntos en función de la campaña de marketing actual realizada por la tienda. 3) Cálculo de impuestos donde el monto del impuesto depende de cada artículo específico del pedido. En su opinión, ¿a dónde pertenecería toda esta lógica? ¿Podría dar un ejemplo simple de pseudocódigo? ¡Gracias!
Nik
4
@Nik En el modelo enriquecido, el pedido tendría una referencia al objeto Cliente y el objeto Cliente tendría una referencia al Programa de fidelización. Por lo tanto, la Orden tendría acceso a toda la información que necesitaba sin necesidad de referencias explícitas a cosas como servicios y repositorios de los que obtener esa información. Sin embargo, parece fácil encontrarse con un caso en el que se produzcan referencias cíclicas. Es decir, referencias de pedidos Cliente, el cliente tiene una lista de todos los pedidos. Creo que esto puede deberse en parte a la razón por la que la gente prefiere Anemic ahora.
Crush
3
@crush El enfoque que describe funciona muy bien. Hay una trampa. Es probable que almacenemos las entidades en una base de datos. Entonces, para calcular el total de un pedido, tenemos que buscar el pedido, el cliente, el programa de lealtad, la campaña de marketing, la tabla de impuestos de la base de datos. Considere también, un Cliente tiene una colección de Pedidos, un Programa de Lealtad tiene una colección de Clientes, etc. Si ingenuamente buscamos todos estos, terminaremos cargando toda la base de datos en la RAM. Esto no es viable, por supuesto, por lo que recurrimos a cargar solo datos relevantes de la base de datos ... 1/2
Nik
3
@Nik "Si obtenemos todos estos de forma nativa, terminaremos cargando toda la base de datos en la RAM". Ese es uno de los principales inconvenientes del modelo rico en mi mente también. El modelo enriquecido es bueno hasta que su dominio se vuelve grande y complejo, y luego comienza a encontrar limitaciones de infraestructura. Sin embargo, ahí es donde los ORM de carga diferida pueden ayudar. Encuentre uno bueno y podrá retener modelos enriquecidos sin cargar toda la base de datos en la memoria cuando solo necesitaba una vigésima parte. Dicho esto, yo mismo tiendo a usar el modelo anémico con CQRS después de muchos años de ir y venir entre anémico y rico.
aplasta el
2
Otra cosa a considerar es dónde vive la lógica de su dominio empresarial. Cada vez más desarrolladores lo están sacando de la base de datos y hacia las aplicaciones a las que pertenece, en mi opinión. Pero si está atrapado en una situación en la que su empresa exige que la lógica empresarial permanezca en la capa de la base de datos (procedimientos almacenados), es casi seguro que no se beneficiará de agregar esa lógica en un modelo de dominio enriquecido. De hecho, podría estar preparándose para encontrarse con conflictos en los que los procedimientos almacenados tienen reglas diferentes a las de la capa de dominio de su aplicación ...
aplasta el
54

Bozhidar Bozhanov parece argumentar a favor del modelo anémico en esta publicación de blog.

Aquí está el resumen que presenta:

  • Los objetos de dominio no deben administrarse mediante Spring (IoC), no deben tener DAO ni nada relacionado con la infraestructura inyectada en ellos

  • los objetos de dominio tienen los objetos de dominio de los que dependen establecidos por hibernación (o el mecanismo de persistencia)

  • Los objetos de dominio realizan la lógica empresarial, como es la idea central de DDD, pero esto no incluye consultas de base de datos o CRUD, solo operaciones en el estado interno del objeto

  • Rara vez se necesitan DTO: los objetos de dominio son los DTO en sí mismos en la mayoría de los casos (lo que ahorra algo de código repetitivo)

  • Los servicios realizan operaciones CRUD, envían correos electrónicos, coordinan los objetos del dominio, generan informes basados ​​en múltiples objetos del dominio, ejecutan consultas, etc.

  • la capa de servicio (aplicación) no es tan delgada, pero no incluye reglas comerciales que son intrínsecas a los objetos de dominio

  • Debe evitarse la generación de código. La abstracción, los patrones de diseño y DI deben usarse para superar la necesidad de generación de código y, en última instancia, para deshacerse de la duplicación de código.

ACTUALIZAR

Recientemente leí este artículo donde el autor aboga por seguir una especie de enfoque híbrido: los objetos de dominio pueden responder varias preguntas basándose únicamente en su estado (que en el caso de modelos totalmente anémicos probablemente se haría en la capa de servicio)

geo y
fuente
11
No puedo extraer de ese artículo que Bozho parezca argumentar a favor del modelo de dominio anémico. la capa de servicio (aplicación) no es tan delgada, pero no incluye reglas comerciales que son intrínsecas a los objetos de dominio . Lo que entiendo es que los objetos de dominio deben contener la lógica empresarial que les es intrínseca, pero no deben contener ninguna otra lógica de infraestructura . Este enfoque no me parece en absoluto un modelo de dominio anémico.
Utku
8
También este: los objetos de dominio realizan la lógica empresarial, como es la idea central de DDD, pero esto no incluye consultas de base de datos o CRUD, solo operaciones sobre el estado interno del objeto . Estas declaraciones no parecen favorecer en absoluto el modelo de dominio anémico. Solo afirman que la lógica de la infraestructura no debe acoplarse a objetos de dominio. Al menos eso es lo que entiendo.
Utku
@Utku En mi opinión, parece bastante claro que Bozho aboga por una especie de híbrido entre los dos modelos, un híbrido que diría que está más cerca del modelo anémico que del modelo rico.
geo y
41

Mi punto de vista es este:

Modelo de dominio anémico = tablas de base de datos asignadas a objetos (solo valores de campo, sin comportamiento real)

Modelo de dominio rico = una colección de objetos que exponen el comportamiento

Si desea crear una aplicación CRUD simple, tal vez un modelo anémico con un marco MVC clásico sea suficiente. Pero si desea implementar algún tipo de lógica, modelo anémico significa que no hará programación orientada a objetos.

* Tenga en cuenta que el comportamiento de los objetos no tiene nada que ver con la persistencia. Una capa diferente (Data Mappers, Repositories, etc.) es responsable de la persistencia de los objetos de dominio.

Jorge
fuente
5
Perdón por mi ignorancia, pero ¿cómo un modelo de dominio rico puede seguir el principio SOLID si pones toda la lógica relacionada con la entidad en la clase? Esto viola el principio SOLID, la 'S' exactamente, que significa responsabilidad única, que dice que una clase solo debe hacer una cosa y hacerlo bien.
redigaffi
6
@redigaffi Depende de cómo se defina "una cosa". Considere una clase con dos propiedades y dos métodos: x, y, sumy difference. Son cuatro cosas. O podría argumentar que es suma y resta (dos cosas). O podrías argumentar que son matemáticas (una cosa). Hay muchas publicaciones de blog sobre cómo encontrar un equilibrio al aplicar SRP. Aquí hay uno: hackernoon.com/…
Rainbolt
2
En DDD, la responsabilidad única significa que una clase / modelo puede administrar su propio estado sin causar efectos secundarios al resto del sistema en su conjunto. En mi experiencia, cualquier otra definición da como resultado tediosos debates filosóficos.
ZombieTfk
12

En primer lugar, copie y pegue la respuesta de este artículo http://msdn.microsoft.com/en-gb/magazine/dn385704.aspx

La Figura 1 muestra un modelo de dominio anémico, que es básicamente un esquema con captadores y definidores.

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; }
}

En este modelo más rico, en lugar de simplemente exponer propiedades para ser leídas y escritas, la superficie pública de Cliente se compone 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
fuente
2
Existe un problema con los métodos que crean un objeto y asignan una propiedad con el objeto recién creado. Hacen que el código sea menos extensible y flexible. 1) ¿Qué pasa si el consumidor de código no quiere crear Address, sino ExtendedAddressheredar Address, con varias propiedades adicionales? 2) ¿O cambiar CustomerCreditCardlos parámetros del constructor para tomar en BankIDlugar de BankName?
Lightman
¿Qué es crear una dirección que requiere servicios adicionales a los que compone el objeto? Le queda el método de inyección para obtener esos servicios. ¿Y si son muchos servicios?
aplastar el
8

Uno de los beneficios de las clases de dominios enriquecidos es que puede llamar a su comportamiento (métodos) cada vez que tenga la referencia al objeto en cualquier capa. Además, tiende a escribir métodos pequeños y distribuidos que colaboran juntos. En las clases de dominio anémicas, tiende a escribir métodos de procedimiento gruesos (en la capa de servicio) que generalmente son impulsados ​​por casos de uso. Por lo general, son menos fáciles de mantener en comparación con las clases de dominio enriquecidas.

Un ejemplo de clases de dominio con comportamientos:

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()
     }

}

El método needToDeliver()devolverá la lista de artículos que deben entregarse, incluida la bonificación. Se puede llamar dentro de la clase, desde otra clase relacionada o desde otra capa. Por ejemplo, si pasa Ordera ver, puede usar needToDeliver()de seleccionado Orderpara mostrar la lista de elementos que debe confirmar el usuario antes de hacer clic en el botón Guardar para conservar el archivo Order.

Responder a un comentario

Así es como uso la clase de dominio del controlador:

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

La creación de Ordery LineItemes en una sola transacción. Si uno de los LineItemno se puede crear, no Orderse creará.

Tiendo a tener métodos que representan una sola transacción, como:

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

Todo deliver()lo que esté dentro se ejecutará como una sola transacción. Si necesito ejecutar muchos métodos no relacionados en una sola transacción, crearía una clase de servicio.

Para evitar la excepción de carga diferida, utilizo el gráfico de entidad con nombre JPA 2.1. Por ejemplo, en el controlador para la pantalla de entrega, puedo crear un método para cargar deliveryatributos e ignorarlos bonus, como repository.findOrderByNumberFetchDelivery(). En la pantalla de bonificación, llamo a otro método que carga el bonusatributo e ignora delivery, como repository.findOrderByNumberFetchBonus(). Esto requiere disciplina ya que todavía no puedo llamar deliver()dentro de la pantalla de bonificación.

jocki
fuente
1
¿Qué tal el alcance de la transacción?
kboom
5
Los comportamientos del modelo de dominio no deben contener lógica de persistencia (incluida la transacción). Deben ser probables (en prueba unitaria) sin estar conectados a la base de datos. El alcance de la transacción es responsabilidad de la capa de servicio o la capa de persistencia.
jocki
1
¿Qué tal la carga diferida entonces?
kboom
Cuando crea instancias de clases de dominio en la prueba unitaria, no están en estado administrado porque son objetos simples. Todos los comportamientos se pueden probar correctamente.
jocki
¿Y qué sucede cuando espera el objeto de dominio de la capa de servicio? ¿No se gestiona entonces?
kboom
8

Cuando solía escribir aplicaciones de escritorio monolíticas, construía modelos de dominio ricos, solía disfrutar construyéndolos.

Ahora escribo microservicios HTTP diminutos, hay la menor cantidad de código posible, incluidos los DTO anémicos.

Creo que DDD y este argumento anémico datan de la era monolítica de las aplicaciones de escritorio o servidor. Recuerdo esa época y estaría de acuerdo en que los modelos anémicos son extraños. Construí una gran aplicación de comercio de divisas monolítica y no había ningún modelo, de verdad, fue horrible.

Con los microservicios, los pequeños servicios con su rico comportamiento son posiblemente los modelos componibles y agregados dentro de un dominio. Por lo tanto, es posible que las implementaciones de microservicios en sí mismas no requieran más DDD. La aplicación de microservicio puede ser el dominio.

Un microservicio de pedidos puede tener muy pocas funciones, expresadas como recursos RESTful o vía SOAP o lo que sea. El código de microservicio de pedidos puede ser extremadamente simple.

Un servicio (micro) único más grande y monolítico, especialmente uno que mantiene su modelo en RAM, puede beneficiarse de DDD.

Luke Puplett
fuente
¿Tiene algún ejemplo de código para microservicios HTTP que represente su estado actual de la técnica? Sin pedirle que escriba nada, solo comparta enlaces si tiene algo que pueda señalar. Gracias.
Casey Plummer
3

Creo que la raíz del problema está en una falsa dicotomía. ¿Cómo es posible extraer estos 2 modelos: rico y "anémico" y contrastarlos entre sí? Creo que solo es posible si tienes ideas equivocadas sobre lo que es una clase . No estoy seguro, pero creo que lo encontré en uno de los videos de Bozhidar Bozhanov en Youtube. Una clase no es un método de datos + sobre estos datos. Es una comprensión totalmente inválida que lleva a la división de clases en dos categorías: solo datos, modelo tan anémico y métodos + datos - tan rico modelo (para ser más correctos, hay una tercera categoría: solo métodos pares).

Lo cierto es que clase es un concepto en algún modelo ontológico, una palabra, una definición, un término, una idea, es un DENOTAT . Y esta comprensión elimina la falsa dicotomía: no se puede tener SOLO modelo anémico o SOLO modelo rico, porque significa que su modelo no es adecuado, no es relevante para la realidad: algunos conceptos solo tienen datos, algunos solo tienen métodos, algunos de ellos se mezclan. Porque tratamos de describir, en este caso, algunas categorías, conjuntos de objetos, relaciones, conceptos con clases, y como sabemos, algunos conceptos son solo procesos (métodos), algunos de ellos son solo conjuntos de atributos (datos), algunos de son relaciones con atributos (mixtos).

Creo que una aplicación adecuada debería incluir todo tipo de clases y evitar fanáticamente autolimitarse a un solo modelo. No importa cómo se represente la lógica: con código o con objetos de datos interpretables (como Free Monads ), de todos modos: deberíamos tener clases (conceptos, denotat) que representen procesos, lógica, relaciones, atributos, características, datos, etc. y no para tratar de evitar algunos de ellos o reducirlos a un solo tipo.

Entonces, podemos extraer la lógica a otra clase y dejar los datos en la original, pero no tiene sentido porque algún concepto puede incluir atributos y relaciones / procesos / métodos y una separación de ellos duplicará el concepto bajo 2 nombres que pueden ser reducido a patrones: "OBJETO-Atributos" y "OBJETO-Lógica". Está bien en los lenguajes procedimentales y funcionales debido a su limitación, pero es un excesivo autocontrol para un lenguaje que te permite describir todo tipo de conceptos.

AleatorioB
fuente
1

Los modelos de dominio anémicos son importantes para ORM y su fácil transferencia a través de redes (la sangre vital de todas las aplicaciones comerciales), pero OO es muy importante para encapsular y simplificar las partes 'transaccionales / de manejo' de su código.

Por eso lo importante es poder identificarse y convertir de un mundo a otro.

Nombre modelos Anemic algo como AnemicUser o UserDAO, etc. para que los desarrolladores sepan que hay una mejor clase para usar, luego tenga un constructor apropiado para la clase none Anemic

User(AnemicUser au)

y método adaptador para crear la clase anémica para transporte / persistencia

User::ToAnemicUser() 

Trate de usar el usuario no anémico en todas partes fuera del transporte / persistencia

Andrew Paté
fuente
-1

Aquí hay un ejemplo que podría ayudar:

Anémico

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

No 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
fuente
Parece que se puede convertir en un ValueObject frente a una entidad.
code5
Solo una copia y pega de Wikipedia sin ninguna explicación
wst
quien escribió antes? @wst
Alireza Rahmani Khalili
@AlirezaRahmaniKhalili según la historia de Wikipedia, han sido los primeros ... A menos que no haya entendido tu pregunta.
wst
-1

El enfoque clásico de DDD no dice evitar a toda costa los modelos anémicos y ricos. Sin embargo, MDA aún puede aplicar todos los conceptos de DDD (contextos limitados, mapas de contexto, objetos de valor, etc.) pero usa modelos Anemic vs Rich en todos los casos. Hay muchos casos en los que el uso de servicios de dominio para orquestar casos de uso de dominio complejos en un conjunto de agregados de dominio es un enfoque mucho mejor que simplemente los agregados que se invocan desde la capa de aplicación. La única diferencia con el enfoque clásico de DDD es ¿dónde residen todas las validaciones y reglas comerciales? Hay una nueva construcción conocida como validadores de modelos. Los validadores garantizan la integridad del modelo de entrada completo antes de que se lleve a cabo cualquier caso de uso o flujo de trabajo de dominio. Las entidades raíz e hijo agregadas son anémicas, pero cada una puede tener sus propios validadores de modelo invocados según sea necesario, por su validador raíz. Los validadores aún se adhieren a SRP, son fáciles de mantener y se pueden probar por unidades.

La razón de este cambio es que ahora nos estamos moviendo más hacia una API en lugar de un primer enfoque de UX para los microservicios. REST ha jugado un papel muy importante en esto. El enfoque de API tradicional (debido a SOAP) se fijó inicialmente en una API basada en comandos frente a los verbos HTTP (POST, PUT, PATCH, GET y DELETE). Una API basada en comandos encaja bien con el enfoque orientado a objetos del modelo enriquecido y sigue siendo muy válida. Sin embargo, las API basadas en CRUD simples, aunque pueden encajar dentro de un modelo enriquecido, se adaptan mucho mejor a modelos anémicos simples, validadores y servicios de dominio para orquestar el resto.

Me encanta DDD en todo lo que tiene que ofrecer, pero llega un momento en que necesitas estirarlo un poco para adaptarse a un enfoque de arquitectura en constante cambio y mejor.

código5
fuente