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

185

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.

codeulike
fuente
Eche un vistazo a mi pregunta stackoverflow.com/q/8269784/235715 , muestra una situación en la que es difícil capturar la lógica, sin que la Entidad tenga acceso al repositorio. Aunque creo que las entidades no deberían tener acceso a los repositorios, y hay una solución para mi situación en la que el código puede reescribirse sin referencia de repositorio, pero actualmente no puedo pensar en ninguno.
Alex Burtsev
No sé de dónde vino. Mis pensamientos: creo que este malentendido proviene de personas que no entienden de qué se trata DDD. Este enfoque no es para implementar software sino para diseñarlo (dominio .. diseño). En aquellos días, teníamos arquitectos e implementadores, pero ahora solo hay desarrolladores de software. DDD está destinado a arquitectos. Y cuando un arquitecto está diseñando software, necesita alguna herramienta o patrón para representar una memoria o base de datos para los desarrolladores que implementarán el diseño preparado. Pero el diseño en sí (desde una perspectiva comercial) no tiene o necesita un repositorio.
berhalak

Respuestas:

47

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.

quertosis
fuente
15
Gracias. Pero debería esforzarme por obtener la mayor lógica de negocios posible en las entidades (y sus fábricas y especificaciones asociadas, etc.), ¿verdad? Pero si ninguno de ellos puede obtener datos a través de Repositorios, ¿cómo se supone que debo escribir alguna lógica comercial (razonablemente complicada)? Por ejemplo: el usuario de Chatroom no puede cambiar su nombre a un nombre que ya haya sido utilizado por otra persona. Me gustaría que esa regla sea incorporada por la entidad ChatUser, pero no es muy fácil de hacer si no puede acceder al repositorio desde allí. ¿Entonces qué debo hacer?
codeulike
Mi respuesta fue mayor de lo que permitiría el cuadro de comentarios, vea la edición.
certosis
66
Su entidad debe saber cómo protegerse del daño. Esto incluye asegurarse de que no pueda entrar en un estado no válido. Lo que está describiendo con el usuario de la sala de chat es la lógica empresarial que reside ADEMÁS de la lógica que la entidad debe mantener válida. La lógica de negocios, como lo que realmente deseas, pertenece a un servicio Chatroom, no a la entidad ChatUser.
Alec
9
Gracias Alec Esa es una forma clara de expresarlo. Pero para mí parece que la regla de oro centrada en el dominio de Evans de "toda la lógica de negocios debe ir en la capa de dominio" está en conflicto con la regla de "las entidades no deben acceder a los repositorios". Puedo vivir con eso si entiendo por qué, pero no puedo encontrar una buena explicación en línea de por qué las entidades no deberían acceder a los repositorios. Evans no parece mencionarlo explícitamente. ¿De dónde vino? Si puede publicar una respuesta que apunte a una buena literatura, podría obtener una recompensa de 50
puntos:)
44
"el suyo no tiene sentido en los pequeños" Este es un gran error que hacen los equipos ... es un proyecto pequeño, como tal puedo hacer esto y aquello ... dejar de pensar así. Muchos de los pequeños proyectos con los que trabajamos terminan convirtiéndose en grandes, debido a los requisitos comerciales. Si haces algo pequeño o grande, hazlo bien.
MeTitus
35

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:

  1. Debemos conocer nuestras intenciones en una solicitud y lo que queremos del dominio, por lo tanto, podemos hacer llamadas al repositorio antes de construir o invocar el comportamiento Agregado. Esto también ayuda a evitar el problema del estado inconsistente en memoria y la necesidad de una carga diferida (consulte este artículo ). 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 de cosas en nuestras entidades.
  3. Podemos usar una especificación para encapsular y comunicar las necesidades de lógica de dominio y pasarla al repositorio en su lugar (un servicio puede organizar estas cosas por nosotros). La especificación puede provenir de la entidad que se encarga de mantener esa invariante. El repositorio interpretará partes de la especificación en su propia implementación de consulta y aplicará reglas de la especificación en los resultados de la consulta. El objetivo es mantener la lógica de dominio en la capa de dominio. También sirve mejor el idioma ubicuo y la comunicación. Imagine decir "especificación de orden vencida" versus decir "orden de filtro de tbl_order donde place_at es menos de 30 minutos antes de sysdate" (vea esta respuesta ).
  4. Hace que el razonamiento sobre el comportamiento de las entidades sea más difícil ya que se viola el Principio de responsabilidad única. Si necesita resolver problemas de almacenamiento / persistencia, sabe a dónde ir y dónde no ir.
  5. Evita el peligro de dar a una entidad acceso bidireccional al estado global (a través de los servicios de repositorio y dominio). Tampoco desea romper el límite de su 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á 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:

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

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):

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 repositorio o servicio de dominio (7) para buscar objetos dependientes antes de invocar el comportamiento Agregado. Un Servicio de aplicaciones de cliente puede controlar esto y luego enviarlo al Agregado:

Continúa mostrando un ejemplo de esto en 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 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:

$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); 
}
martillo programador
fuente
1
Pregunta: Siempre se nos enseña a no crear un objeto en un estado inválido o inconsistente. Cuando carga usuarios desde el repositorio y luego llama 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?
Jimbo
El repositorio llama al dominio para renovar una instancia. No obtienes una instancia de Usuario sin pasar por el Dominio. El problema que aborda esta respuesta es al revés. Donde el dominio hace referencia al repositorio, y esto debe evitarse.
prograhammer
28

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 ...

Magnus Backeus
fuente
1
Buen punto. El modelo de dominio de la vieja escuela probablemente tendría a la Entidad B responsable de validarse a sí misma antes de dejarse persistir, supongo. ¿Estás seguro de que Evans menciona entidades que no usan repositorios? Estoy a mitad de camino a través del libro y no ha mencionado todavía ...
codeulike
Bueno, leí el libro hace varios años (bueno 3 ...) y mi memoria me falla. No puedo recordar si lo expresó exactamente PERO, sin embargo, creo que ilustró esto a través de ejemplos. También puede encontrar una interpretación comunitaria de su ejemplo de Cargo (de su libro) en dddsamplenet.codeplex.com . Descargue el proyecto de código (mire el proyecto Vanilla, es el ejemplo del libro). Verá que los repositorios solo se usan en la capa de aplicación para acceder a las entidades de dominio.
Magnus Backeus
1
Al descargar el ejemplo DDD SmartCA del libro p2p.wrox.com/… , verá otro enfoque (aunque este es un cliente RIA de Windows) donde los repositorios se usan en los servicios (nada extraño aquí) pero los servicios se usan dentro de las entidades. Esto es algo que no haría PERO soy un tipo de aplicación webb. Dado el escenario para la aplicación SmartCA donde debe poder trabajar sin conexión, tal vez el diseño ddd se verá diferente.
Magnus Backeus
El ejemplo de SmartCA suena interesante, ¿en qué capítulo está? (las descargas de códigos están organizadas por capítulo)
codeulike
1
@codeulike Actualmente estoy diseñando e implementando un marco utilizando conceptos ddd. A veces, hacer la validación necesita acceder a la base de datos y consultarla (por ejemplo, consultar la comprobación del índice único de varias columnas). Con respecto a esto y al hecho de que las consultas deben escribirse en la capa del repositorio, resulta que las entidades de dominio deben tener referencias a sus interfaces de repositorio en la capa del modelo de dominio para colocar la validación completamente en la capa del modelo de dominio. Entonces, ¿finalmente está bien que las entidades de dominio tengan acceso a los repositorios?
Karamafrooz
13

¿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.

  • Desea mantener una estrecha conexión entre el modelo de dominio y el código.
  • Separar las preocupaciones técnicas ayuda a demostrar que el modelo es práctico para la implementación
  • Desea que el lenguaje ubicuo 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 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:

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 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.

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

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í:

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

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.

ttg
fuente
Gracias, revisaré los enlaces. Pero en su respuesta, ¿está confundiendo 'separación de preocupaciones' con 'ningún acceso a ella'? Ciertamente, la mayoría de las personas estarían de acuerdo en que la capa de persistencia debe mantenerse separada de la capa en la que se encuentran las Entidades. Pero eso es diferente a decir 'las entidades no deberían ser capaces de ver la capa de persistencia, incluso a través de una implementación muy general. interfaz'.
codeulike
Cargando datos a través de una interfaz o no, todavía le preocupa cargar datos mientras implementa las reglas comerciales. Sin embargo, estoy de acuerdo en que mucha gente todavía llama a esta separación de preocupaciones, tal vez el principio de responsabilidad única hubiera sido un mejor término para usar.
ttg
1
¿No está seguro de cómo analizar su último comentario, pero creo que está sugiriendo que los datos no se carguen mientras se procesan las reglas comerciales? Veo que haría que las reglas sean "más puras". Pero muchos tipos de reglas de negocio necesitarán referirse a otros datos. ¿Está sugiriendo que debería cargarse previamente por un objeto separado?
codeulike 05 de
@codeulike: he actualizado mi respuesta. Todavía puede cargar datos durante las reglas de negocio si considera que tiene que hacerlo absolutamente, pero eso no requiere agregar líneas de código de acceso a datos en su modelo de dominio (por ejemplo, carga diferida). En los modelos de dominio que he diseñado, los datos generalmente se cargan por adelantado como usted dijo. Descubrí que la ejecución de reglas comerciales generalmente no requiere una cantidad excesiva de datos.
ttg
12

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).

// 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 instancia 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 ser 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. (¿Dices un servicio de notas de crédito? Ni siquiera voy a hacer nada con las notas de crédito ...)
  • Haría difícil instanciar, por lo tanto, difícil de probar .
  • El problema se propaga fácilmente, ya que otras entidades que contienen esta 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 del método y el doble despacho proporcionan 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 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 la Invoiceentidad.

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 de IInvoiceServiceayuda 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 en SetStatus()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 tener IInvoiceServiceimplementada esa interfaz. El código del cliente simplemente puede usar su single IInvoiceServicepara todos los Invoicemé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.

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

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.

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 un archivo Invoicedesde 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 el Invoice.

Por otro lado, el código que crea un nuevo real Invoicesimplemente pasará una función que devuelve una lista vacía:

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

(Una costumbre ILazy<out T>podría librarnos del reparto feo IEnumerable, pero eso complicaría la discusión).

// Or just an empty IEnumerable
new Invoice(inv => IEnumerable.Empty<CreditNote>())

¡Me encantaría escuchar sus opiniones, preferencias y mejoras!

Timo
fuente
3

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:

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

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 de cliente puede controlar esto.

Alireza Rahmani Khalili
fuente
Pero no de una entidad.
ssmith
De la fuente IDDD de Vernon Vaughn: el calendario de clase pública extiende EventSourcedRootEntity {... public CalendarEntry scheduleCalendarEntry (CalendarIdentityService aCalendarIdentityService,
Teimuraz
revise su periódico @Teimuraz
Alireza Rahmani Khalili
1

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 Repositorycapa (también conocida como Data Accesscapa) 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 ;-)

umlcat
fuente
1
Gracias, pero no estoy preguntando acerca de cómo separar el acceso a datos de la lógica de negocios, eso es algo muy claro en lo que existe un acuerdo muy amplio. Me pregunto por qué en arquitecturas DDD como S # arp, las Entidades ni siquiera pueden 'hablar' con la capa de acceso a datos. Es un arreglo interesante sobre el que no he podido encontrar mucha discusión.
codeulike
0

¿Esto vino del libro Eric Evans Domain Driven Design, o vino de otro lado?

Son cosas viejas. El libro de Eric lo hizo 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 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.

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)

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.

Arnis Lapsa
fuente
Correcto. Entonces, ¿cómo una Entidad ignorante de persistencia implementa Business Logic si ni siquiera se le permite hablar con la capa de persistencia? ¿Qué hace cuando necesita mirar valores en otras entidades arbitrarias?
codeulike
Si su entidad necesita mirar valores en otras entidades arbitrarias, probablemente tenga algunos problemas de diseño. Quizás considere dividir las clases para que sean más coherentes.
cdaq
0

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.

Edvard Englund
fuente
-1

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.

vsingh
fuente
No, esto introduce un acoplamiento innecesario a las entidades, viola el SRP y la separación de preocupaciones y dificulta la deserialización de la persistencia de la entidad (ya que el proceso de deserialización ahora también debe inyectar los servicios / repositorios que la entidad requiere).
ssmith