Modelos de dominio enriquecido: ¿cómo, exactamente, encaja el comportamiento?

84

En el debate de los modelos de dominio Rich vs.Anemic, Internet está llena de consejos filosóficos pero con pocos ejemplos autorizados. El objetivo de esta pregunta es encontrar pautas definitivas y ejemplos concretos de modelos de diseño impulsados ​​por dominio adecuados. (Idealmente en C #.)

Para un ejemplo del mundo real, esta implementación de DDD parece estar mal:

Los siguientes modelos de dominio de WorkItem no son más que bolsas de propiedades, utilizadas por Entity Framework para una base de datos de código primero. Por Fowler, es anémico .

La capa WorkItemService es aparentemente una percepción errónea común de los Servicios de dominio; contiene todo el comportamiento / lógica de negocios para el WorkItem. Según Yemelyanov y otros, es de procedimiento . (pág. 6)

Entonces, si lo siguiente está mal, ¿cómo puedo corregirlo?
El comportamiento, es decir, AddStatusUpdate o Checkout , debe pertenecer a la clase WorkItem ¿correcto?
¿Qué dependencias debería tener el modelo WorkItem?

ingrese la descripción de la imagen aquí

public class WorkItemService : IWorkItemService {
    private IUnitOfWorkFactory _unitOfWorkFactory;

    //using Unity for dependency injection
    public WorkItemService(IUnitOfWorkFactory unitOfWorkFactory) {
        _unitOfWorkFactory = unitOfWorkFactory;
    }

    public void AddStatusUpdate(int workItemId, int statusId) {

        using (var unitOfWork = _unitOfWorkFactory.GetUnitOfWork<IWorkItemUnitOfWork>()) {
            var workItemRepo = unitOfWork.WorkItemRepository;
            var workItemStatusRepo = unitOfWork.WorkItemStatusRepository;

            var workItem = workItemRepo.Read(wi => wi.Id == workItemId).FirstOrDefault();
            if (workItem == null)
                throw new ArgumentException(string.Format(@"The provided WorkItem Id '{0}' is not recognized", workItemId), "workItemId");

            var status = workItemStatusRepo.Read(s => s.Id == statusId).FirstOrDefault();
            if (status == null)
                throw new ArgumentException(string.Format(@"The provided Status Id '{0}' is not recognized", statusId), "statusId");

            workItem.StatusHistory.Add(status);

            workItemRepo.Update(workItem);
            unitOfWork.Save();
        }
    }
}

(Este ejemplo se simplificó para que sea más legible. El código definitivamente sigue siendo complicado, porque es un intento confuso, pero el comportamiento del dominio fue: actualizar el estado agregando el nuevo estado al historial del archivo. En última instancia, estoy de acuerdo con las otras respuestas, esto podría ser manejado por CRUD).

Actualizar

@AlexeyZimarev dio la mejor respuesta, un video perfecto sobre el tema en C # por Jimmy Bogard, pero aparentemente fue trasladado a un comentario a continuación porque no proporcionó suficiente información más allá del enlace. Tengo un borrador de mis notas que resumen el video en mi respuesta a continuación. No dude en comentar la respuesta con cualquier corrección. El video dura una hora pero vale la pena verlo.

Actualización - 2 años después

Creo que es una señal de la naciente madurez de DDD que incluso después de estudiarlo durante 2 años, todavía no puedo prometer que sé la "forma correcta" de hacerlo. El lenguaje ubicuo, las raíces agregadas y su enfoque del diseño basado en el comportamiento son las valiosas contribuciones de DDD a la industria. La ignorancia de la persistencia y el abastecimiento de eventos causa confusión, y creo que una filosofía como esa lo detiene de una adopción más amplia. Pero si tuviera que volver a hacer este código, con lo que he aprendido, creo que se vería así:

ingrese la descripción de la imagen aquí

Todavía agradezco cualquier respuesta a esta publicación (muy activa) que proporcione cualquier código de mejores prácticas para un modelo de dominio válido.

RJB
fuente
66
Todas las teorías filosóficas caen directamente al suelo cuando las cuenta "I don't want to duplicate all my entities into DTOs simply because I don't need it and it violates DRY, and I also don't want my client application to take a dependency on EntityFramework.dll". "Entidades" en la jerga de Entity Framework no son lo mismo que "Entidades" como en "Modelo de dominio"
Federico Berasategui
Estoy de acuerdo con duplicar mis entidades de dominio en un DTO, usando una herramienta automatizada como Automapper, si eso es lo que se necesita. No estoy seguro de cómo se supone que se verá eso al final del día.
RJB
16
Te recomendaría que veas la sesión NDC 2012 de Jimmy Bogard "Elaboración de modelos de dominio perverso" en Vimeo . Explica qué dominio rico debería ser y cómo implementarlo en la vida real teniendo un comportamiento en sus entidades. Los ejemplos son muy prácticos y todos en C #.
Alexey Zimarev
Gracias, estoy a la mitad del video y hasta ahora es perfecto. Sabía que si esto estaba mal, tenía que haber una respuesta "correcta" en algún lugar ...
RJB
2
También exijo amor por Java: /
uylmz

Respuestas:

59

La respuesta más útil fue dada por Alexey Zimarev y obtuvo al menos 7 votos a favor antes de que un moderador la moviera a un comentario debajo de mi pregunta original ...

Su respuesta:

Te recomendaría que veas la sesión NDC 2012 de Jimmy Bogard "Elaboración de modelos de dominio perverso" en Vimeo. Explica qué dominio rico debería ser y cómo implementarlos en la vida real teniendo un comportamiento en sus entidades. Los ejemplos son muy prácticos y todos en C #.

http://vimeo.com/43598193

Tomé algunas notas para resumir el video para beneficio de mi equipo y para proporcionar un poco más de detalle inmediato en esta publicación. (El video dura una hora, pero realmente vale cada minuto si tiene tiempo. Jimmy Bogard merece mucho crédito por su explicación).

  • "Para la mayoría de las aplicaciones ... no sabemos que van a ser complejas cuando empecemos. Simplemente se vuelven así".
    • La complejidad crece naturalmente a medida que se agregan códigos y requisitos. Las aplicaciones pueden comenzar de manera muy simple, como CRUD, pero el comportamiento / las reglas se pueden incorporar.
    • "Lo bueno es que no tenemos que comenzar de manera compleja. Podemos comenzar con el modelo de dominio anémico, eso es solo bolsas de propiedades, y con solo técnicas de refactorización estándar podemos avanzar hacia un modelo de dominio verdadero".
  • Modelos de dominio = objetos comerciales. Comportamiento del dominio = reglas comerciales.
  • El comportamiento a menudo está oculto en una aplicación; puede estar en PageLoad, Button1_Click o, a menudo, en clases auxiliares como 'FooManager' o 'FooService'.
  • Las reglas de negocio que están separadas de los objetos de dominio "requieren que recordemos" esas reglas.
    • En mi ejemplo personal anterior, una regla de negocio es WorkItem.StatusHistory.Add (). No solo estamos cambiando el estado, sino que lo estamos archivando para auditoría.
  • Los comportamientos de dominio "eliminan errores en una aplicación con mucha más facilidad que simplemente escribir un montón de pruebas". Las pruebas requieren que sepas escribir esas pruebas. Los comportamientos de dominio le ofrecen los caminos correctos para probar .
  • Los servicios de dominio son "clases auxiliares para coordinar actividades entre diferentes entidades de modelo de dominio".
    • Servicios de dominio! = Comportamiento del dominio. Las entidades tienen comportamiento, los servicios de dominio son solo intermediarios entre las entidades.
  • Los objetos de dominio no deben poseer la infraestructura que necesitan (es decir, IOfferCalculatorService). El servicio de infraestructura debe pasarse al modelo de dominio que lo usa.
  • Los modelos de dominio deberían ofrecerle información sobre lo que pueden hacer, y solo deberían poder hacer esas cosas.
  • Las propiedades de los modelos de dominio deben protegerse con setters privados, de modo que solo el modelo pueda establecer sus propias propiedades, a través de sus propios comportamientos . De lo contrario, es "promiscuo".
  • Los objetos de modelo de dominio anémico, que son solo bolsas de propiedades para un ORM, son solo "una capa delgada, una versión fuertemente tipada sobre la base de datos".
    • "Por fácil que sea poner una fila de la base de datos en un objeto, eso es lo que tenemos".
    • 'La mayoría de los modelos de objetos persistentes son solo eso. Lo que diferencia un modelo de dominio anémico frente a una aplicación que realmente no tiene comportamiento, es si un objeto tiene reglas de negocio, pero esas reglas no se encuentran en un modelo de dominio. '
  • "Para muchas aplicaciones, no hay una necesidad real de crear ningún tipo de capa lógica de aplicación empresarial real, es algo que puede comunicarse con la base de datos y quizás una forma fácil de representar los datos que están allí".
    • En otras palabras, si todo lo que está haciendo es CRUD sin objetos comerciales especiales o reglas de comportamiento, no necesita DDD.

Por favor, siéntase libre de comentar con cualquier otro punto que considere que debería incluirse, o si cree que alguna de estas notas está fuera de lugar. Intenté citar directamente o parafrasear tanto como sea posible.

RJB
fuente
Gran video especialmente para ver cómo funciona la refactorización en una herramienta. Mucho se trata de la encapsulación adecuada de los objetos de dominio (para asegurarse de que sean consistentes). Hace un gran trabajo contando las reglas de negocios sobre ofertas, miembros, etc. Menciona la palabra invariante un par de veces (que es el modelado de dominio basado en contratos). Desearía que el código .net comunicara mejor lo que es una regla comercial formal, ya que estos cambian y usted necesita mantenerlos.
Fuhrmanator
6

Su pregunta no puede ser respondida, porque su ejemplo es incorrecto. Específicamente, porque no hay comportamiento. Al menos no en el área de su dominio. El ejemplo del AddStatusUpdatemétodo no es una lógica de dominio, sino una lógica que usa ese dominio. Ese tipo de lógica tiene sentido estar dentro de algún tipo de servicio, que maneja solicitudes externas.

Por ejemplo, si existía el requisito de que un elemento de trabajo específico solo puede tener estados específicos, o que solo puede tener N estados, entonces esa es la lógica del dominio y debe ser parte de uno WorkItemo de StatusHistoryun método.

La razón de su confusión es porque está tratando de aplicar una directriz al código que no la necesita. Los modelos de dominio solo son relevantes si tiene mucha lógica de dominio compleja. P.ej. lógica que funciona en las propias entidades y se deriva de los requisitos. Si el código trata de manipular entidades de datos externos, entonces eso no es, muy probablemente, una lógica de dominio. Pero en el momento en que obtienes muchos correos ifelectrónicos basados ​​en los datos y entidades con los que estás trabajando, eso es lógica de dominio.

Uno de los problemas del modelado de dominio verdadero es que se trata de administrar requisitos complejos. Y como tal, su verdadero poder y beneficios no se pueden mostrar en un código simple. Necesita docenas de entidades con toneladas de requisitos a su alrededor para ver realmente los beneficios. Nuevamente, su ejemplo es demasiado simple para que el modelo de dominio realmente brille.

Finalmente, algo de OT que mencionaría es que un verdadero modelo de dominio con diseño real de OOP sería realmente difícil de seguir utilizando Entity Framework. Si bien los ORM se diseñaron con el mapeo de la verdadera estructura OOP a las relacionales, todavía hay muchos problemas, y el modelo relacional a menudo se filtrará en el modelo OOP. Incluso con nHibernate, que considero mucho más poderoso que EF, esto puede ser un problema.

Eufórico
fuente
Buenos puntos. ¿Dónde pertenecería entonces el método AddStatusUpdate, en Datos u otro proyecto en Infraestructura? ¿Cuál es un ejemplo de cualquier comportamiento que teóricamente pueda pertenecer a WorkItem? Cualquier código psuedo o maqueta sería muy apreciado. Mi ejemplo en realidad fue simplificado para ser más legible. Hay otras entidades y, por ejemplo, AddStatusUpdate tiene un comportamiento adicional: en realidad toma un nombre de categoría de estado y, si esa categoría no existe, se crea la categoría.
RJB
@RJB Como dije, AddStatusUpdate es un código que usa el dominio. Entonces, ya sea algún tipo de servicio web o aplicación que utiliza las clases de dominio. Y como dije, no puede esperar ningún tipo de maqueta o pseudocódigo, porque necesitaría hacer un proyecto completo de complejidad lo suficientemente grande como para mostrar una ventaja real del modelo de dominio OOP.
Eufórico el
5

Su suposición de que encapsular su lógica de negocios asociada con WorkItem en un "servicio pesado" es un antipatrón inherente que diría no necesariamente.

Independientemente de lo que piense sobre el modelo de dominio anémico, los patrones y prácticas estándar típicos de una aplicación .NET de Line of Business fomentan un enfoque de capas transaccionales compuesto por varios componentes. Fomentan la separación de la lógica empresarial del modelo de dominio específicamente para facilitar la comunicación de un modelo de dominio común a través de otros componentes .NET, así como componentes en diferentes conjuntos de tecnología o niveles físicos.

Un ejemplo de esto sería un servicio web SOAP basado en .NET que se comunica con una aplicación cliente Silverlight que tiene una DLL que contiene tipos de datos simples. Este proyecto de entidad de dominio podría integrarse en un ensamblado .NET o un ensamblaje Silverlight, donde los componentes Silverlight interesados ​​que tengan esta DLL no estarán expuestos a comportamientos de objetos que pueden depender de componentes solo disponibles para el servicio.

Independientemente de su postura en este debate, este es el patrón adoptado y aceptado presentado por Microsoft y, en mi opinión profesional, no es un enfoque incorrecto, pero un modelo de objeto que define su propio comportamiento tampoco es necesariamente un antipatrón. Si continúa con este diseño, lo mejor es darse cuenta y comprender algunas de las limitaciones y puntos débiles con los que se puede encontrar si necesita integrarse con otros componentes que necesitan ver su modelo de dominio. En ese caso particular, tal vez desee que un traductor convierta su modelo de dominio de estilo orientado a objetos en objetos de datos simples que no expongan ciertos métodos de comportamiento.

árbol de arce
fuente
1
1) ¿Cómo se puede separar la lógica empresarial del modelo de dominio? Es el dominio en el que vive esta lógica de negocios; Las entidades en ese dominio están ejecutando el comportamiento asociado con esa lógica de negocios. El mundo real no tiene servicios, ni existen en la cabeza de expertos en dominios. 2) Cualquier componente que desee integrarse con usted necesita construir su propio modelo de dominio, porque sus necesidades serán diferentes y tendrá una visión diferente de su modelo de dominio. Es una falacia de larga data que puede crear un modelo de dominio que se puede compartir.
Stefan Billiet
1
@StefanBilliet Esos son buenos puntos sobre la falacia de un modelo de dominio universal, pero es posible en componentes más simples e interacción de componentes como lo he hecho antes. Mi opinión es que la lógica de traducción entre modelos de dominio puede generar mucho código tedioso y repetitivo, y si se puede evitar de manera segura, puede ser una buena opción de diseño.
maple_shaft
1
Para ser sincero, creo que la única buena opción de diseño es un modelo sobre el que pueda razonar un experto en negocios. Estás creando un modelo de dominio para que una empresa lo use para resolver ciertos problemas dentro de ese dominio. Dividir el comportamiento de las entidades de dominio en servicios hace que sea más difícil para todos los involucrados, porque constantemente tienes que mapear lo que un experto en dominios dice al código de servicio que casi no se parece a la conversación actual. En mi experiencia, pierdes mucho más tiempo con eso que escribiendo repetitivo. Eso no quiere decir que no hay formas de evitar el código de curso de caldera.
Stefan Billiet
@StefanBilliet En un mundo perfecto, estoy de acuerdo con usted donde un experto en negocios tiene tiempo para sentarse con los desarrolladores. La realidad de la industria del software es que el experto en negocios no tiene tiempo o interés en involucrarse en este nivel o peor, sin embargo, se espera que los desarrolladores solo lo resuelvan con una vaga orientación.
maple_shaft
Es cierto, pero esa no es una razón para aceptar esa realidad. Continuar en esa búsqueda es perder el tiempo (y posiblemente la reputación) de los desarrolladores y el dinero del cliente. El proceso que describí es una relación que debe construirse con el tiempo; requiere mucho esfuerzo, pero produce resultados mucho mejores. Hay una razón por la que el "Idioma ubicuo" a menudo se considera el aspecto más importante de DDD.
Stefan Billiet
5

Me doy cuenta de que esta pregunta es bastante antigua, así que esta respuesta es para la posteridad. Quiero responder con un ejemplo concreto en lugar de uno basado en la teoría.

Encapsule el "cambio del estado del elemento de trabajo" en la WorkItemclase de la siguiente manera:

public SomeStatusUpdateType Status { get; private set; }

public void ChangeStatus(SomeStatusUpdateType status)
{
    // Maybe we designed this badly at first ;-)
    Status = status;       
}

Ahora su WorkItemclase es responsable de mantenerse en un estado legal. Sin embargo, la implementación es bastante débil. El propietario del producto quiere un historial de todas las actualizaciones de estado realizadas en WorkItem.

Lo cambiamos a algo como esto:

private ICollection<SomeStatusUpdateType> StatusUpdates { get; private set; }
public SomeStatusUpdateType Status => StatusUpdates.OrderByDescending(s => s.CreatedOn).FirstOrDefault();

public void ChangeStatus(SomeStatusUpdateType status)
{
    // Better...
    StatusUpdates.Add(status);       
}

La implementación ha cambiado drásticamente, pero la persona que llama del ChangeStatusmétodo desconoce los detalles subyacentes de la implementación y no tiene ninguna razón para cambiarse.

Este es un ejemplo de una entidad modelo de dominio rico, en mi humilde opinión.

Don
fuente