En Domain Driven Design, parece que hay un montón de acuerdo que las Entidades no debe acceder a repositorios directamente.
¿Esto vino del libro Eric Evans Domain Driven Design , o vino de otro lado?
¿Dónde hay algunas buenas explicaciones para el razonamiento detrás de esto?
editar: Para aclarar: no estoy hablando de la práctica clásica de OO de separar el acceso a datos en una capa separada de la lógica de negocios: estoy hablando de la disposición específica por la cual en DDD, las entidades no deben hablar con los datos capa de acceso (es decir, se supone que no deben contener referencias a objetos del repositorio)
Actualización: le di la recompensa a BacceSR porque su respuesta parecía la más cercana, pero todavía estoy bastante oculto sobre esto. Si es un principio tan importante, ¿debería haber algunos buenos artículos al respecto en línea en algún lugar, seguramente?
actualización: marzo de 2013, los votos a favor de la pregunta implican que hay mucho interés en esto, y aunque ha habido muchas respuestas, todavía creo que hay espacio para más si la gente tiene ideas sobre esto.
Respuestas:
Hay un poco de confusión aquí. Los repositorios acceden a las raíces agregadas. Las raíces agregadas son entidades. La razón de esto es la separación de preocupaciones y una buena estratificación. Esto no tiene sentido en proyectos pequeños, pero si está en un equipo grande quiere decir: "Accede a un producto a través del Repositorio de productos. El producto es una raíz agregada para una colección de entidades, incluido el objeto ProductCatalog. Si desea actualizar el ProductCatalog, debe pasar por el ProductRepository ".
De esta manera, tiene una separación muy, muy clara en la lógica de negocios y dónde se actualizan las cosas. No tienes un hijo que está solo y escribe todo este programa que hace todas estas cosas complicadas en el catálogo de productos y cuando se trata de integrarlo en el proyecto original, estás sentado allí mirándolo y dándote cuenta todo tiene que ser abandonado. También significa que cuando las personas se unen al equipo, agregan nuevas funciones, saben a dónde ir y cómo estructurar el programa.
¡Pero espera! El repositorio también se refiere a la capa de persistencia, como en el Patrón de repositorio. En un mundo mejor, el repositorio y el patrón de repositorio de Eric Evans tendrían nombres separados, porque tienden a superponerse bastante. Para obtener el patrón de repositorio, tiene contraste con otras formas en que se accede a los datos, con un bus de servicio o un sistema de modelo de evento. Por lo general, cuando llegas a este nivel, la definición del repositorio de Eric Evans se deja de lado y comienzas a hablar de un contexto acotado. Cada contexto acotado es esencialmente su propia aplicación. Es posible que tenga un sofisticado sistema de aprobación para incluir cosas en el catálogo de productos. En su diseño original, el producto era la pieza central, pero en este contexto acotado es el catálogo de productos. Aún puede acceder a la información del producto y actualizar el producto a través de un bus de servicio,
De vuelta a su pregunta original. Si está accediendo a un repositorio desde una entidad, significa que la entidad realmente no es una entidad comercial, sino probablemente algo que debería existir en una capa de servicio. Esto se debe a que las entidades son objeto de negocio y deben preocuparse por ser lo más parecido posible a un DSL (lenguaje específico de dominio). Solo tenga información comercial en esta capa. Si está solucionando un problema de rendimiento, sabrá buscar en otro lado, ya que solo la información de la empresa debe estar aquí. Si de repente, tiene problemas de aplicación aquí, está haciendo que sea muy difícil extender y mantener una aplicación, lo cual es realmente el corazón de DDD: hacer un software que se pueda mantener.
Respuesta al comentario 1 : Correcto, buena pregunta. Entonces, no toda la validación ocurre en la capa de dominio. Sharp tiene un atributo "DomainSignature" que hace lo que quieres. Es consciente de la persistencia, pero ser un atributo mantiene limpia la capa de dominio. Asegura que no tenga una entidad duplicada con, en su ejemplo, el mismo nombre.
Pero hablemos de reglas de validación más complicadas. Digamos que eres Amazon.com. ¿Alguna vez ha pedido algo con una tarjeta de crédito vencida? Sí, donde no actualicé la tarjeta y compré algo. Acepta el pedido y la interfaz de usuario me informa que todo es color de rosa. Unos 15 minutos más tarde, recibiré un correo electrónico indicando que hay un problema con mi pedido, mi tarjeta de crédito no es válida. Lo que sucede aquí es que, idealmente, hay alguna validación de expresiones regulares en la capa de dominio. ¿Es este un número de tarjeta de crédito correcto? En caso afirmativo, persista el pedido. Sin embargo, hay una validación adicional en la capa de tareas de la aplicación, donde se consulta un servicio externo para ver si se puede realizar el pago en la tarjeta de crédito. De lo contrario, no envíe nada, suspenda el pedido y espere al cliente.
No tenga miedo de crear objetos de validación en la capa de servicio que puedan acceder a los repositorios. Solo manténgalo fuera de la capa de dominio.
fuente
Al principio, fui persuasivo para permitir que algunas de mis entidades accedan a repositorios (es decir, carga diferida sin un ORM). Más tarde llegué a la conclusión de que no debería y que podía encontrar formas alternativas:
Vernon Vaughn en el libro rojo Implementing Domain-Driven Design se refiere a este tema en dos lugares que conozco (nota: este libro está totalmente respaldado por Evans como puedes leer en el prólogo). En el Capítulo 7 sobre Servicios, usa un servicio de dominio y una especificación para evitar la necesidad de que un agregado use un repositorio y otro agregado para determinar si un usuario está autenticado. Se le cita diciendo:
Vernon, Vaughn (06-02-2013). Implementación de diseño basado en dominio (Kindle Location 6089). Educación Pearson. Versión Kindle.
Y en el Capítulo 10 sobre Agregados, en la sección titulada "Modelo de navegación" , dice (justo después de que recomienda el uso de ID únicos globales para hacer referencia a otras raíces agregadas):
Continúa mostrando un ejemplo de esto en código:
Continúa mencionando también otra solución de cómo se puede usar un servicio de dominio en un método de comando Agregado junto con el envío doble . (No puedo recomendar lo beneficioso que es leer su libro. Después de que te hayas cansado de hurgar sin cesar en Internet, busca el dinero merecido y lee el libro).
Luego tuve una discusión con el siempre amable Marco Pivetta @Ocramius, quien me mostró un poco de código para extraer una especificación del dominio y usar eso:
1) Esto no se recomienda:
2) En un servicio de dominio, esto es bueno:
fuente
getFriends()
antes de hacer cualquier otra cosa, estará vacío o cargado de forma diferida. Si está vacío, este objeto está en un estado no válido. Tiene alguna idea sobre esto?Es una muy buena pregunta. Esperaré alguna discusión sobre esto. Pero creo que se menciona en varios libros de DDD y Jimmy Nilssons y Eric Evans. Supongo que también es visible a través de ejemplos de cómo usar el patrón de repositorio.
PERO vamos a discutir. Creo que un pensamiento muy válido es ¿por qué una entidad debe saber cómo persistir a otra entidad? Importante con DDD es que cada entidad tiene la responsabilidad de administrar su propia "esfera de conocimiento" y no debe saber nada sobre cómo leer o escribir otras entidades. Claro que probablemente solo puede agregar una interfaz de repositorio a la Entidad A para leer las Entidades B. Pero el riesgo es que exponga el conocimiento sobre cómo persistir B. ¿La entidad A también validará en B antes de persistir B en db?
Como puede ver, la entidad A puede involucrarse más en el ciclo de vida de la entidad B y eso puede agregar más complejidad al modelo.
Supongo (sin ningún ejemplo) que las pruebas unitarias serán más complejas.
Pero estoy seguro de que siempre habrá escenarios en los que tengas la tentación de usar repositorios a través de entidades. Tienes que mirar cada escenario para hacer un juicio válido. Pros y contras. Pero la solución de entidad de repositorio en mi opinión comienza con muchos Contras. Debe ser un escenario muy especial con Pros que equilibren los Contras ...
fuente
¿Por qué separar el acceso a datos?
Del libro, creo que las dos primeras páginas del capítulo Diseño impulsado por el modelo dan alguna justificación de por qué desea resumir los detalles de implementación técnica de la implementación del modelo de dominio.
Esto parece ser todo con el propósito de evitar un "modelo de análisis" separado que se divorcia de la implementación real del sistema.
Por lo que entiendo del libro, dice que este "modelo de análisis" puede terminar siendo diseñado sin considerar la implementación de software. Una vez que los desarrolladores intentan implementar el modelo comprendido por el lado comercial, forman sus propias abstracciones debido a la necesidad, causando un muro en la comunicación y la comprensión.
En la otra dirección, los desarrolladores que introducen demasiadas preocupaciones técnicas en el modelo de dominio también pueden causar esta división.
Por lo tanto, podría considerar que practicar la separación de preocupaciones, como la persistencia, puede ayudar a proteger contra estos diseños de modelos de análisis divergentes. Si se siente necesario introducir cosas como la persistencia en el modelo, entonces es una señal de alerta. Quizás el modelo no sea práctico para la implementación.
Citando:
"El modelo único reduce las posibilidades de error, porque el diseño es ahora una consecuencia directa del modelo cuidadosamente considerado. El diseño, e incluso el código en sí, tiene la comunicabilidad de un modelo".
De la forma en que interpreto esto, si terminas con más líneas de código que tratan cosas como el acceso a la base de datos, pierdes esa comunicación.
Si la necesidad de acceder a una base de datos es para cosas como verificar la unicidad, eche un vistazo a:
Udi Dahan: los mayores errores que cometen los equipos al aplicar DDD
http://gojko.net/2010/06/11/udi-dahan-the-biggest-mistakes-teams-make-when-applying-ddd/
bajo "Todas las reglas no son iguales"
y
Empleando el patrón del modelo de dominio
http://msdn.microsoft.com/en-us/magazine/ee236415.aspx#id0400119
en "Escenarios para no usar el modelo de dominio", que toca el mismo tema.
Cómo separar el acceso a datos
Cargando datos a través de una interfaz
La "capa de acceso a datos" se ha abstraído a través de una interfaz, a la que se llama para recuperar los datos requeridos:
Pros: La interfaz separa el código de plomería de "acceso a datos", lo que le permite seguir escribiendo pruebas. El acceso a los datos se puede manejar caso por caso, lo que permite un mejor rendimiento que una estrategia genérica.
Contras: El código de llamada debe asumir lo que se ha cargado y lo que no.
Digamos que GetOrderLines devuelve objetos OrderLine con una propiedad ProductInfo nula por razones de rendimiento. El desarrollador debe tener un conocimiento profundo del código detrás de la interfaz.
He probado este método en sistemas reales. Terminas cambiando el alcance de lo que se carga todo el tiempo en un intento de solucionar problemas de rendimiento. Termina mirando detrás de la interfaz para ver el código de acceso a datos para ver qué se carga y qué no.
Ahora, la separación de las preocupaciones debería permitir al desarrollador centrarse en un aspecto del código a la vez, tanto como sea posible. La técnica de interfaz elimina el CÓMO se cargan estos datos, pero no CUÁNTO se cargan los datos, CUÁNDO se cargan y DÓNDE se cargan.
Conclusión: ¡Separación bastante baja!
Carga lenta
Los datos se cargan a pedido. Las llamadas para cargar datos están ocultas dentro del gráfico del objeto en sí, donde acceder a una propiedad puede hacer que se ejecute una consulta SQL antes de devolver el resultado.
Ventajas: El "CUÁNDO, DÓNDE y CÓMO" del acceso a datos está oculto para el desarrollador, centrándose en la lógica de dominio. No hay código en el agregado que se ocupe de cargar datos. La cantidad de datos cargados puede ser la cantidad exacta requerida por el código.
Contras: cuando te encuentras con un problema de rendimiento, es difícil de solucionar cuando tienes una solución genérica de "talla única". La carga diferida puede empeorar el rendimiento general e implementar la carga diferida puede ser complicado.
Interfaz de roles / Recolección ansiosa
Cada caso de uso se hace explícito a través de una interfaz de rol implementada por la clase agregada, lo que permite manejar las estrategias de carga de datos por caso de uso.
La estrategia de recuperación puede verse así:
Entonces su agregado puede verse así:
BillOrderFetchingStrategy se usa para construir el agregado, y luego el agregado hace su trabajo.
Pros: Permite un código personalizado por caso de uso, lo que permite un rendimiento óptimo. Está en línea con el Principio de segregación de interfaz . No hay requisitos de código complejos. Las pruebas unitarias de agregados no tienen que imitar la estrategia de carga. La estrategia de carga genérica se puede utilizar para la mayoría de los casos (por ejemplo, una estrategia de "cargar todo") y se pueden implementar estrategias de carga especiales cuando sea necesario.
Contras: el desarrollador todavía tiene que ajustar / revisar la estrategia de recuperación después de cambiar el código de dominio.
Con el enfoque de la estrategia de recuperación, es posible que aún se encuentre cambiando el código de recuperación personalizado por un cambio en las reglas comerciales. No es una separación perfecta de preocupaciones, pero terminará siendo más fácil de mantener y es mejor que la primera opción. La estrategia de obtención encapsula los datos de CÓMO, CUÁNDO y DÓNDE se cargan. Tiene una mejor separación de preocupaciones, sin perder flexibilidad, ya que el tamaño único se adapta a todos los enfoques de carga diferida.
fuente
Encontré que este blog tiene argumentos bastante buenos en contra de encapsular repositorios dentro de entidades:
http://thinkbeforecoding.com/post/2009/03/04/How-not-to-inject-services-in-entities
fuente
Que excelente pregunta. Estoy en el mismo camino del descubrimiento, y la mayoría de las respuestas en Internet parecen traer tantos problemas como soluciones.
Entonces (a riesgo de escribir algo con lo que no estoy de acuerdo dentro de un año) aquí están mis descubrimientos hasta ahora.
En primer lugar, nos gusta un modelo de dominio rico , que nos brinda una alta capacidad de descubrimiento (de lo que podemos hacer con un agregado) y legibilidad (llamadas a métodos expresivos).
Queremos lograr esto sin inyectar ningún servicio en el constructor de una entidad, porque:
¿Cómo, entonces, podemos hacer esto? Mi conclusión hasta ahora es que las dependencias del método y el doble despacho proporcionan una solución decente.
CreateCreditNote()
ahora requiere un servicio responsable de crear notas de crédito. Utiliza el despacho doble , descargando completamente el trabajo al servicio responsable, mientras mantiene la capacidad de detección de laInvoice
entidad.SetStatus()
ahora tiene una dependencia simple en un registrador, que obviamente realizará parte del trabajo .Para lo último, para facilitar las cosas en el código del cliente, podríamos iniciar sesión a través de un
IInvoiceService
. Después de todo, el registro de facturas parece bastante intrínseco a una factura. Este tipo deIInvoiceService
ayuda ayuda a evitar la necesidad de todo tipo de miniservicios para diversas operaciones. La desventaja es que se hace oscurecer qué es exactamente lo que el servicio va a hacer . Incluso podría comenzar a parecer un despacho doble, mientras que la mayor parte del trabajo todavía se hace enSetStatus()
sí mismo.Todavía podríamos nombrar el parámetro 'logger', con la esperanza de revelar nuestra intención. Sin embargo, parece un poco débil.
En cambio, optaría por solicitar un
IInvoiceLogger
(como ya lo hacemos en el ejemplo de código) y tenerIInvoiceService
implementada esa interfaz. El código del cliente simplemente puede usar su singleIInvoiceService
para todos losInvoice
métodos que soliciten un 'miniservicio' intrínseco a la factura muy particular, mientras que las firmas de métodos todavía dejan en claro lo que están pidiendo.Noto que no he abordado los repositorios de forma explícita. Bueno, el registrador es o usa un repositorio, pero permítanme también proporcionar un ejemplo más explícito. Podemos usar el mismo enfoque, si el repositorio se necesita solo en uno o dos métodos.
De hecho, esto proporciona una alternativa a las cargas perezosas siempre problemáticas .
Actualización: he dejado el texto a continuación con fines históricos, pero sugiero evitar el 100% de las cargas perezosas.
Para los verdaderos cargas perezosos, basadas en la propiedad, yo no uso actualmente la inyección de constructor, pero de una manera persistencia-ignorante.
Por un lado, un repositorio que carga un archivo
Invoice
desde la base de datos puede tener acceso libre a una función que cargará las notas de crédito correspondientes e inyectará esa función en elInvoice
.Por otro lado, el código que crea un nuevo real
Invoice
simplemente pasará una función que devuelve una lista vacía:(Una costumbre
ILazy<out T>
podría librarnos del reparto feoIEnumerable
, pero eso complicaría la discusión).¡Me encantaría escuchar sus opiniones, preferencias y mejoras!
fuente
Para mí, esto parece ser una buena práctica general relacionada con OOD en lugar de ser específica para DDD.
Las razones que se me ocurren son:
fuente
simplemente Vernon Vaughn da una solución:
fuente
Aprendí a codificar la programación orientada a objetos antes de que apareciera todo este zumbido de capas separadas, y mis primeros objetos / clases se mapearon directamente a la base de datos.
Finalmente, agregué una capa intermedia porque tuve que migrar a otro servidor de base de datos. He visto / escuchado sobre el mismo escenario varias veces.
Creo que separar el acceso a los datos (también conocido como "Repositorio") de la lógica de su negocio es una de esas cosas, que se han reinventado varias veces, a pesar del libro de diseño impulsado por el dominio, lo hacen mucho "ruido".
Actualmente uso 3 capas (GUI, lógica, acceso a datos), como lo hacen muchos desarrolladores, porque es una buena técnica.
La separación de los datos en una
Repository
capa (también conocida comoData Access
capa) puede verse como una buena técnica de programación, no solo como una regla a seguir.Al igual que muchas metodologías, es posible que desee comenzar, NO implementado, y eventualmente, actualizar su programa, una vez que los entienda.
Cita: La Ilíada no fue totalmente inventada por Homero, Carmina Burana no fue totalmente inventada por Carl Orff, y en ambos casos, la persona que puso a otros a trabajar, todos juntos, obtuvo el crédito ;-)
fuente
Son cosas viejas. El libro de Eric lo hizo zumbar un poco más.
La razón es simple: la mente humana se debilita cuando se enfrenta a contextos múltiples vagamente relacionados. Conducen a la ambigüedad (América en América del Sur / América del Norte significa América del Sur / América del Norte), la ambigüedad conduce a un mapeo constante de la información cada vez que la mente "la toca" y eso se resume en mala productividad y errores.
La lógica empresarial debe reflejarse lo más claramente posible. Las claves foráneas, la normalización, el mapeo relacional de objetos son de un dominio completamente diferente; esas cosas son técnicas, relacionadas con la computadora.
En analogía: si está aprendiendo a escribir a mano, no debería estar agobiado con la comprensión de dónde se hizo la pluma, por qué la tinta retiene el papel, cuándo se inventó el papel y cuáles son otros inventos chinos famosos.
La razón sigue siendo la misma que mencioné anteriormente. Aquí es solo un paso más allá. ¿Por qué las entidades deberían ser parcialmente persistentes ignorantes si pueden ser (al menos cercanas a) totalmente? Menos preocupaciones no relacionadas con el dominio que tiene nuestro modelo, más espacio para respirar que nuestra mente tiene cuando tiene que reinterpretarlo.
fuente
Para citar a Carolina Lilientahl, "Los patrones deberían evitar los ciclos" https://www.youtube.com/watch?v=eJjadzMRQAk , donde se refiere a las dependencias cíclicas entre clases. En el caso de los repositorios dentro de los agregados, existe la tentación de crear dependencias cíclicas por conveniencia de la navegación de objetos como la única razón. El patrón mencionado anteriormente por prograhammer, que fue recomendado por Vernon Vaughn, donde otros agregados son referenciados por identificadores en lugar de instancias raíz, (¿hay un nombre para este patrón?) Sugiere una alternativa que podría guiar a otras soluciones.
Ejemplo de dependencia cíclica entre clases (confesión):
(Time0): Dos clases, Sample y Well, se refieren entre sí (dependencia cíclica). El pozo se refiere a la muestra, y la muestra se refiere nuevamente al pozo, por conveniencia (a veces, hacer un loop de muestras, a veces hacer un loop de todos los pocillos en una placa). No podía imaginar casos en los que Sample no hiciera referencia al Pozo donde está ubicado.
(Tiempo 1): un año después, se implementan muchos casos de uso ... y ahora hay casos en los que la Muestra no debe volver a hacer referencia al Pozo en el que se encuentra. Hay placas temporales dentro de un paso de trabajo. Aquí un pozo se refiere a una muestra, que a su vez se refiere a un pozo en otra placa. Debido a esto, a veces se produce un comportamiento extraño cuando alguien intenta implementar nuevas funciones. Toma tiempo para penetrar.
También me ayudó este artículo mencionado anteriormente sobre los aspectos negativos de la carga diferida.
fuente
En el mundo ideal, DDD propone que las entidades no deberían tener referencia a las capas de datos. pero no vivimos en el mundo ideal. Es posible que los dominios deban referirse a otros objetos de dominio para la lógica de negocios con los que podrían no tener una dependencia. Es lógico que las entidades se refieran a la capa del repositorio con fines de solo lectura, para obtener los valores.
fuente