oop domain-driven-design repository-pattern s#arp-architecture

oop - DDD: la regla de que las entidades no pueden acceder a los repositorios directamente



domain-driven-design repository-pattern (11)

En el Diseño Dirigido por Dominio, parece haber un lots agreement que las Entidades no deberían acceder directamente a los Repositorios.

¿Esto vino del libro de Diseño impulsado por el dominio de Eric Evans, o vino de otro lugar?

¿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, no se supone que contengan referencias a los objetos del repositorio)

actualización: le di la recompensa a BacceSR porque su respuesta parecía más cercana, pero todavía estoy bastante en la oscuridad sobre esto. Si es un principio tan importante, seguramente debería haber algunos buenos artículos sobre él en línea en alguna parte.

Actualización: Marzo de 2013, las votaciones ascendentes sobre 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 al respecto.


¿Esto vino del libro de Diseño impulsado por el dominio de Eric Evans, o vino de otro lugar?

Es cosas viejas El libro de Eric acaba de hacer zumbar un poco más.

¿Dónde hay algunas buenas explicaciones para el razonamiento detrás de esto?

La razón es simple: la mente humana se debilita cuando enfrenta contextos múltiples vagamente relacionados. Conducen a la ambigüedad (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 cuando la mente "lo toca" y lo resume como una 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, están relacionadas con la computadora.

En analogía: si estás aprendiendo a escribir a mano, no deberías abrumarte con la comprensión de dónde se hizo la pluma, por qué la tinta retiene en el papel, cuándo se inventó el papel y cuáles son otros inventos chinos famosos.

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, no se supone que contengan referencias a los objetos del repositorio)

La razón sigue siendo la misma que mencioné anteriormente. Aquí está solo un paso más allá. ¿Por qué las entidades deben ser parcialmente persistentes ignorantes si pueden ser (al menos cerca de) totalmente? Menos preocupaciones de dominio no relacionadas con nuestro modelo: más espacio para respirar que obtiene nuestra mente cuando tiene que volver a interpretarlo.


¿Por qué separar el acceso a los datos?

Del libro, creo que las dos primeras páginas del capítulo Model Driven Design dan alguna justificación de por qué desea abstraer los detalles técnicos de implementación de la implementación del modelo de dominio.

  • Desea mantener una conexión estrecha entre el modelo de dominio y el código
  • La separación de las preocupaciones técnicas ayuda a demostrar que el modelo es práctico para la implementación
  • Desea que el lenguaje omnipresente penetre en el diseño del sistema

Esto parece ser todo con el propósito de evitar un "modelo de análisis" separado que se divorcie de la implementación real del sistema.

Según lo que entiendo del libro, dice que este "modelo de análisis" puede terminar siendo diseñado sin considerar la implementación del software. Una vez que los desarrolladores intentan implementar el modelo entendido 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 y los modelos de análisis divergen. Si es necesario introducir cosas como la persistencia en el modelo, entonces es una bandera roja. Tal vez 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 capacidad de comunicación de un modelo".

La forma en que estoy interpretando esto, si terminaste con más líneas de código que tratan sobre cosas como el acceso a bases de datos, pierdes esa capacidad de comunicación.

Si la necesidad de acceder a una base de datos es para cosas como comprobar la singularidad, eche un vistazo a:

Udi Dahan: los mayores errores que los equipos cometen al aplicar DDD

http://gojko.net/2010/06/11/udi-dahan-the-biggest-mistakes-teams-make-when-applying-ddd/

en "Todas las reglas no son iguales"

y

Empleando el patrón de 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 los 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 llama para recuperar los datos requeridos:

var orderLines = OrderRepository.GetOrderLines(orderId); foreach (var line in orderLines) { total += line.Price; }

Pros: la interfaz separa el código de plomería de "acceso a datos", lo que le permite escribir pruebas. El acceso a los datos puede manejarse 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 motivos 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. Usted termina cambiando el alcance de lo que está cargado todo el tiempo en un intento de solucionar problemas de rendimiento. Usted 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 enfocarse 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ántos datos se cargan, CUÁNDO se carga y DÓNDE se cargan.

Conclusión: ¡Bastante baja separación!

Carga lenta

Los datos se cargan a pedido. Las llamadas a datos de carga se ocultan dentro del propio gráfico de objetos, donde el acceso a una propiedad puede hacer que se ejecute una consulta SQL antes de devolver el resultado.

foreach (var line in order.OrderLines) { total += line.Price; }

Pros: El ''CUÁNDO, DÓNDE y CÓMO'' del acceso a los datos está oculto para el desarrollador, centrándose en la lógica del dominio. No hay ningún 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 se ve afectado por un problema de rendimiento, es difícil solucionarlo cuando tiene una solución genérica de "talla única". La carga diferida puede empeorar el rendimiento en general, y la implementación de la carga diferida puede ser complicada.

Interfaz de rol / Obtención ansiosa

Cada caso de uso se hace explícito a través de una interfaz de roles implementada por la clase agregada, lo que permite manejar las estrategias de carga de datos por caso de uso.

La estrategia de obtención puede verse así:

public class BillOrderFetchingStrategy : ILoadDataFor<IBillOrder, Order> { Order Load(string aggregateId) { var order = new Order(); order.Data = GetOrderLinesWithPrice(aggregateId); return order; } }

Entonces su agregado puede verse así:

public class Order : IBillOrder { void BillOrder(BillOrderCommand command) { foreach (var line in this.Data.OrderLines) { total += line.Price; } etc... } }

El BillOrderFetchingStrategy se usa para construir el agregado, y luego el agregado hace su trabajo.

Pros: permite 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 . Sin 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 usar para la mayoría de los casos (por ejemplo, una estrategia de "cargar todo") y se pueden implementar estrategias especiales de carga cuando sea necesario.

Contras: El desarrollador todavía tiene que ajustar / revisar la estrategia de búsqueda después de cambiar el código de dominio.

Con el enfoque de estrategia de búsqueda, es posible que todavía encuentre que cambia el código de búsqueda personalizado para 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 búsqueda encapsula los datos CÓMO, CUÁNDO y DÓNDE se carga. Tiene una mejor separación de preocupaciones, sin perder flexibilidad, como el enfoque de carga única y holgada de una talla.


Al principio, fui persuasivo para permitir que algunas de mis entidades accedieran a repositorios (es decir, carga lenta sin un ORM). Más tarde llegué a la conclusión de que no debería y que podría encontrar formas alternativas:

  1. Deberíamos conocer nuestras intenciones en una solicitud y lo que queremos del dominio, por lo tanto, podemos realizar llamadas al repositorio antes de construir o invocar el comportamiento Agregado. Esto también ayuda a evitar el problema de inconsistencia en el estado de la memoria y la necesidad de una carga diferida (consulte este article ). El olor es que ya no puede crear una instancia en memoria de su entidad sin preocuparse por el acceso a los datos.
  2. CQS (Command Query Separation) puede ayudar a reducir la necesidad de querer llamar al repositorio para cosas en nuestras entidades.
  3. Podemos usar una specification para encapsular y comunicar las necesidades de lógica de dominio y pasarlas al repositorio en su lugar (un servicio puede orquestar estas cosas para nosotros). La especificación puede venir de la entidad que está a cargo de mantener ese invariante. El repositorio interpretará partes de la especificación en su propia implementación de consulta y aplicará las reglas de la especificación en los resultados de la consulta. Esto apunta a mantener la lógica de dominio en la capa de dominio. También sirve mejor el lenguaje ubicuo y la comunicación. Imagine que dice "especificación de orden vencida" frente a decir "orden de filtro desde tbl_order donde colocado_at es menos de 30 minutos antes de sysdate" (vea esta answer ).
  4. Hace que el razonamiento sobre el comportamiento de las entidades sea más difícil ya que se viola el Principio de Responsabilidad Individual. Si necesita resolver problemas de almacenamiento / persistencia, sabe a dónde ir y dónde no ir.
  5. Evita el peligro de otorgar a una entidad acceso bidireccional al estado global (a través del repositorio y los servicios de dominio). Tampoco quiere romper su límite de transacción.

Vernon Vaughn en el libro rojo Implementing Domain-Driven Design se refiere a este tema en dos lugares que conozco (nota: este libro está completamente respaldado por Evans, como puede 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. Él es citado diciendo:

Como regla general, debemos tratar de evitar el uso de Repositorios (12) desde Agregados internos, si es posible.

Vernon, Vaughn (2013-02-06). Implementación del diseño controlado por el dominio (ubicación del Kindle 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):

La referencia por identidad no impide por completo la navegación a través del modelo. Algunos usarán un Repositorio (12) desde dentro de un Agregado para la búsqueda. Esta técnica se llama Modelo de dominio desconectado, y en realidad es una forma de carga diferida. Sin embargo, hay un enfoque recomendado diferente: use un servicio de repositorio o de dominio (7) para buscar objetos dependientes antes de invocar el comportamiento agregado. Un Servicio de aplicación cliente puede controlar esto y luego enviarlo al Agregado:

Él va a mostrar un ejemplo de esto en el código:

public class ProductBacklogItemService ... { ... @Transactional public void assignTeamMemberToTask( String aTenantId, String aBacklogItemId, String aTaskId, String aTeamMemberId) { BacklogItem backlogItem = backlogItemRepository.backlogItemOfId( new TenantId( aTenantId), new BacklogItemId( aBacklogItemId)); Team ofTeam = teamRepository.teamOfId( backlogItem.tenantId(), backlogItem.teamId()); backlogItem.assignTeamMemberToTask( new TeamMemberId( aTeamMemberId), ofTeam, new TaskId( aTaskId)); } ... }

Continúa para mencionar 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 se haya cansado de hurgar sin parar en Internet, ahorre el dinero que se merece y lea el libro).

Luego tuve una discussion con el siempre amable Marco Pivetta @Ocramius que me mostró un poco de código para sacar una especificación del dominio y usar eso:

1) Esto no es recomendado:

$user->mountFriends(); // <-- has a repository call inside that loads friends?

2) En un servicio de dominio, esto es bueno:

public function mountYourFriends(MountFriendsCommand $mount) { /* see http://store.steampowered.com/app/296470/ */ $user = $this->users->get($mount->userId()); $friends = $this->users->findBySpecification($user->getFriendsSpecification()); array_map([$user, ''mount''], $friends); }


Aprendí a codificar la programación orientada a objetos antes de que aparezca este zumbido de capa separado, y mis primeros objetos / clases DID se asignaron directamente a la base de datos.

Eventualmente, 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 tu lógica comercial es una de esas cosas, que se han reinventado varias veces, aunque el libro de Diseño impulsado por el dominio hace que sea un montón de "ruido".

Actualmente uso 3 capas (GUI, Lógica, Acceso a datos), como muchos desarrolladores, porque es una buena técnica.

Separar los datos, en una capa de Repository (también conocida como capa de Data Access ), 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, por NO implementado, y, eventualmente, actualizar su programa, una vez que lo 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 ;-)


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 necesiten hacer referencia 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 de repositorio con fines de solo lectura para obtener los valores.



Es una muy buena pregunta. Espero con interés alguna discusión sobre esto. Pero creo que se menciona en varios libros de DDD y en Jimmy nilssons y Eric Evans. Supongo que también es visible a través de ejemplos de cómo usar el patrón de repositorios.

PERO vamos a discutir. Creo que un pensamiento muy válido es por qué una entidad debería 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 pueda 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á 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 Cons.


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: "Usted accede a un producto a través del Repositorio de Producto. Producto es una raíz agregada para una colección de entidades, incluido el objeto ProductCatalog. Si quiere actualizar el ProductCatalog, debe ir a través del ProductRepository ".

De esta forma, tiene una separación muy, muy clara en la lógica de negocios y donde las cosas se actualizan. No tiene un niño que está solo y escribe todo el programa que hace todas estas cosas complicadas en el catálogo de productos y cuando se trata de integrarlo al proyecto aguas arriba, está sentado allí mirándolo y dándose cuenta todo tiene que ser abandonado. También significa 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, un Repositorio de Eric Evans y el Patrón de Repositorio tendrían nombres separados, porque tienden a superponerse bastante. Para obtener el patrón de repositorio, tiene un contraste con otras formas de acceder 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 de Repositorio de Eric Evans pasa por el lado y comienzas a hablar de un contexto delimitado. Cada contexto delimitado es esencialmente su propia aplicación. Es posible que tenga un sistema de aprobación sofisticado para incluir cosas en el catálogo de productos. En su diseño original, el producto fue la pieza central, pero en este contexto limitado, el catálogo de productos es. Todavía puede acceder a la información del producto y actualizar el producto a través de un bus de servicio, pero debe tener en cuenta que un catálogo de productos fuera del contexto delimitado puede significar algo completamente diferente.

De vuelta a tu pregunta original. Si está accediendo a un repositorio desde dentro de una entidad, significa que la entidad no es realmente una entidad comercial, sino probablemente algo que debería existir en una capa de servicio. Esto se debe a que las entidades son un objeto comercial y deben preocuparse por ser lo más parecido posible a un DSL (lenguaje específico de dominio). Solo tiene información comercial en esta capa. Si está solucionando un problema de rendimiento, sabrá buscar en otra parte, ya que solo la información comercial debe estar aquí. Si de repente, tiene problemas de aplicación aquí, está haciendo muy difícil extender y mantener una aplicación, que es realmente el corazón de DDD: hacer un software que se pueda mantener.

Respuesta al Comentario 1 : Bien, buena pregunta. Entonces, no toda la validación ocurre en la capa de dominio. Sharp tiene un atributo "DomainSignature" que hace lo que quiere. Es consciente de la persistencia, pero al ser un atributo mantiene limpia la capa de dominio. Asegura que no tiene 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? Lo hice, donde no actualicé la tarjeta y compré algo. Acepta el pedido y la IU me informa que todo es color de rosa. Unos 15 minutos más tarde, recibiré un correo electrónico que dice que hay un problema con mi pedido, mi tarjeta de crédito no es válida. Lo que está sucediendo aquí es que, idealmente, hay alguna validación de expresiones regulares en la capa de dominio. ¿Este es un número correcto de tarjeta de crédito? 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. Esto debería tener lugar en una capa de servicio.

No tenga miedo de crear objetos de validación en la capa de servicio que pueda acceder a los repositorios. Solo manténlo fuera de la capa de dominio.


Para mí, esto parece ser una buena práctica general relacionada con OOD en lugar de ser específica de DDD.

Las razones que puedo pensar son:

  • Separación de inquietudes (las entidades deben separarse de la forma en que persisten, ya que podría haber múltiples estrategias en las que se persistiría la misma entidad según el escenario de uso)
  • Lógicamente, las entidades podrían verse en un nivel por debajo del nivel en el que operan los repositorios. Los componentes de nivel inferior no deberían tener conocimiento sobre los componentes de nivel superior. Por lo tanto, las entradas no deberían tener conocimiento sobre los repositorios.

Que excelente pregunta Estoy en el mismo camino de descubrimiento, y la mayoría de las respuestas a través de Internet parecen traer tantos problemas como soluciones.

Entonces (a riesgo de escribir algo con lo que no estoy de acuerdo en un año a partir de ahora) aquí están mis descubrimientos hasta ahora.

En primer lugar, nos gusta un modelo de dominio enriquecido , que nos ofrece alta capacidad de descubrimiento (de lo que podemos hacer con un agregado) y legibilidad (llamadas de método expresivo).

// Entity public class Invoice { ... public void SetStatus(StatusCode statusCode, DateTime dateTime) { ... } public void CreateCreditNote(decimal amount) { ... } ... }

Queremos lograr esto sin inyectar ningún servicio en el constructor de una entidad, porque:

  • La introducción de un nuevo comportamiento (que usa un nuevo servicio) podría conducir a un cambio de constructor, lo que significa que el cambio afecta a cada línea que crea una instancia de la entidad .
  • Estos servicios no son parte del modelo , pero la inyección del constructor sugeriría que sí.
  • A menudo, un servicio (incluso su interfaz) es un detalle de implementación en lugar de una parte del dominio. El modelo de dominio tendría una dependencia externa .
  • Puede ser confuso por qué la entidad no puede existir sin estas dependencias. (¿Un servicio de nota de crédito, dices? Ni siquiera voy a hacer nada con notas de crédito ...)
  • Sería una instancia difícil, por lo tanto, difícil de probar .
  • El problema se propaga fácilmente, porque otras entidades que lo contienen obtendrían las mismas dependencias, que en ellas pueden parecer dependencias muy poco naturales .

¿Cómo, entonces, podemos hacer esto? Mi conclusión hasta ahora es que las dependencias de métodos y el doble despacho brindan una solución decente.

public class Invoice { ... // Simple method injection public void SetStatus(IInvoiceLogger logger, StatusCode statusCode, DateTime dateTime) { ... } // Double dispatch public void CreateCreditNote(ICreditNoteService creditNoteService, decimal amount) { creditNoteService.CreateCreditNote(this, amount); } ... }

CreateCreditNote() ahora requiere un servicio que sea responsable de crear notas de crédito. Utiliza despacho doble , descargando completamente el trabajo al servicio responsable, a la vez que mantiene la Invoice entidad Invoice .

SetStatus() ahora tiene una dependencia simple en un registrador, que obviamente realizará parte del trabajo .

Para este último, para facilitar las cosas en el código del cliente, podríamos en su lugar iniciar sesión en un IInvoiceService . Después de todo, el registro de facturas parece bastante intrínseco a una factura. Tal IInvoiceService único ayuda a evitar la necesidad de todo tipo de mini-servicios para diversas operaciones. La desventaja es que se vuelve oscuro qué exactamente hará ese servicio. Incluso podría comenzar a parecer un despacho doble, mientras que la mayoría del trabajo realmente se realiza en SetStatus() mismo.

Todavía podríamos nombrar el parámetro ''logger'', con la esperanza de revelar nuestra intención. Parece un poco débil, sin embargo.

En cambio, optaría por solicitar un IInvoiceLogger (como ya lo hacemos en el ejemplo del código) y que IInvoiceService implemente esa interfaz. El código del cliente puede simplemente usar su único IInvoiceService para todos los métodos de Invoice que solicitan un "servicio-servicio" tan particular, intrínseco a la factura, mientras que las firmas de métodos aún dejan muy claro lo que están pidiendo.

Noté que no me he dirigido a 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 es necesario en uno o dos métodos.

public class Invoice { public IEnumerable<CreditNote> GetCreditNotes(ICreditNoteRepository repository) { ... } }

De hecho, esto proporciona una alternativa a las cargas perezosas siempre problemáticas. Simplemente podemos hacer que la Invoice recuerde las notas de crédito que ha cargado, y evitar volver a cargarlas.

Para cargas perezosas verdaderas basadas en propiedades, actualmente uso la inyección de constructor, pero de forma persistente.

public class Invoice { // Lazy could use an interface (for contravariance if nothing else), but I digress public Lazy<IEnumerable<CreditNote>> CreditNotes { get; } // Give me something that will provide my credit notes public Invoice(Func<Invoice, IEnumerable<CreditNote>> lazyCreditNotes) { this.CreditNotes = new Lazy<IEnumerable<CreditNotes>>() => lazyCreditNotes(this)); } }

Por un lado, un repositorio que carga una Invoice de 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 la Invoice .

Por otro lado, el código que crea una nueva Invoice real simplemente pasará una función que devuelve una lista vacía:

new Invoice(inv => new List<CreditNote>() as IEnumerable<CreditNote>)

(Una ILazy<out T> podría liberarnos del hechizo feo en IEnumerable , pero eso complicaría la discusión).

¡Estaré feliz de escuchar sus opiniones, preferencias y mejoras!


simplemente Vernon Vaughn da una solución:

Use un repositorio o servicio de dominio para buscar objetos dependientes antes de invocar el comportamiento agregado. Un servicio de aplicación cliente puede controlar esto.