¿El aislamiento del modelo de dominio / persistencia suele ser tan incómodo?

12

Me estoy sumergiendo en los conceptos del diseño controlado por dominio (DDD) y encontré algunos principios extraños, especialmente con respecto al aislamiento del dominio y el modelo de persistencia. Aquí está mi comprensión básica:

  1. Un servicio en la capa de aplicación (que proporciona un conjunto de características) solicita objetos de dominio de un repositorio que necesita para llevar a cabo su función.
  2. La implementación concreta de este repositorio obtiene datos del almacenamiento para el que se implementó
  3. El servicio le dice al objeto de dominio, que encapsula la lógica de negocios, que realice ciertas tareas que modifican su estado.
  4. El servicio le dice al repositorio que persista el objeto de dominio modificado.
  5. El repositorio necesita asignar el objeto de dominio nuevamente a la representación correspondiente en el almacenamiento.

Ilustración de flujo

Ahora, dados los supuestos anteriores, lo siguiente parece incómodo:

Anuncio 2 .:

El modelo de dominio parece cargar todo el objeto de dominio (incluidos todos los campos y referencias), incluso si no son necesarios para la función que lo solicitó. La carga completa podría no ser posible si se hace referencia a otros objetos de dominio, a menos que cargue también esos objetos de dominio y todos los objetos a los que hacen referencia, y así sucesivamente. Me viene a la mente la carga diferida, lo que, sin embargo, significa que comienza a consultar los objetos de su dominio, que deberían ser responsabilidad del repositorio en primer lugar.

Dado este problema, la forma "correcta" de cargar objetos de dominio parece tener una función de carga dedicada para cada caso de uso. Estas funciones dedicadas solo cargarían los datos requeridos por el caso de uso para el que fueron diseñados. Aquí es donde entra en juego la incomodidad: en primer lugar, tendría que mantener una cantidad considerable de funciones de carga para cada implementación del repositorio, y los objetos de dominio terminarían en estados incompletos nullen sus campos. Esto último técnicamente no debería ser un problema porque si un valor no se cargó, no debería ser requerido por la funcionalidad que lo solicitó de todos modos. Aún así es incómodo y un peligro potencial.

Anuncio 3 .:

¿Cómo verificaría un objeto de dominio las restricciones de unicidad en la construcción si no tiene ninguna noción del repositorio? Por ejemplo, si quisiera crear un nuevo Usercon un número de seguridad social único (que se proporciona), el conflicto más temprano ocurriría al pedirle al repositorio que guarde el objeto, solo si hay una restricción de unicidad definida en la base de datos. De lo contrario, podría buscar un Usercon la seguridad social dada e informar un error en caso de que exista, antes de crear uno nuevo. Pero entonces las comprobaciones de restricciones vivirían en el servicio y no en el objeto de dominio al que pertenecen. Me acabo de dar cuenta de que los objetos de dominio pueden usar repositorios (inyectados) para la validación.

Anuncio 5 .:

Percibo la asignación de objetos de dominio a un backend de almacenamiento como un proceso de trabajo intensivo en comparación con hacer que los objetos de dominio modifiquen los datos subyacentes directamente. Es, por supuesto, un requisito previo esencial para desacoplar la implementación de almacenamiento concreto del código de dominio. Sin embargo, ¿realmente tiene un costo tan alto?

Aparentemente tiene la opción de usar herramientas ORM para hacer el mapeo por usted. Sin embargo, esto a menudo requeriría que diseñe el modelo de dominio de acuerdo con las restricciones de ORM, o incluso que introduzca una dependencia del dominio a la capa de infraestructura (mediante el uso de anotaciones de ORM en los objetos de dominio, por ejemplo). También he leído que los ORM introducen una sobrecarga computacional considerable.

En el caso de las bases de datos NoSQL, para las cuales casi no existen conceptos similares a ORM, ¿cómo hace un seguimiento de qué propiedades cambiaron en los modelos de dominio save()?

Editar : Además, para que un repositorio acceda al estado del objeto de dominio (es decir, el valor de cada campo), el objeto de dominio debe revelar su estado interno que rompe la encapsulación.

En general:

  • ¿A dónde iría la lógica transaccional? Esto es ciertamente persistencia específica. Es posible que algunas infraestructuras de almacenamiento ni siquiera admitan transacciones (como los repositorios simulados en memoria).
  • Para las operaciones masivas que modifican múltiples objetos, ¿tendría que cargar, modificar y almacenar cada objeto individualmente para pasar por la lógica de validación encapsulada del objeto? Eso se opone a ejecutar una sola consulta directamente en la base de datos.

Agradecería alguna aclaración sobre este tema. ¿Son correctas mis suposiciones? Si no, ¿cuál es la forma correcta de abordar estos problemas?

Doble m
fuente
1
Buenos puntos y preguntas, también me interesan esos. Una nota al margen: si está modelando correctamente el agregado, eso significa que en cualquier momento dado de existencia, la instancia de agregado debe estar en un estado válido, ese es el punto principal del agregado (y no usar un agregado como contenedor de composición). Eso también significa que para restaurar el agregado de los datos de la base de datos, el repositorio en sí mismo generalmente tendría que usar un constructor específico y un conjunto de operaciones de mutación, y no veo cómo un ORM podría saber automáticamente cómo hacer esas operaciones .
Dusan
2
Lo que es aún más decepcionante es que esas preguntas como la suya se formulan con bastante frecuencia, pero, que yo sepa, hay CERO ejemplos de la implementación de los agregados y repositorios que figuran en el libro
Dusan

Respuestas:

5

Su comprensión básica es correcta y la arquitectura que bosqueja es buena y funciona bien.

¿Leyendo entre líneas parece que viene de un estilo de programación de registro activo más centrado en la base de datos? Para llegar a una implementación funcional, diría que necesitas

1: Los objetos de dominio no tienen que incluir todo el gráfico de objetos. Por ejemplo, podría tener:

public class Customer
{
    public string AddressId {get;set;}
    public string Name {get;set;}
}

public class Address
{
    public string Id {get;set;}
    public string HouseNumber {get;set;
}

La dirección y el cliente solo deben ser parte del mismo agregado si tiene alguna lógica como "el nombre del cliente solo puede comenzar con la misma letra que el nombre de la casa". Tienes razón al evitar la carga diferida y las versiones 'Lite' de los objetos.

2: Las restricciones de unicidad generalmente son competencia del repositorio, no del objeto de dominio. No inyecte repositorios en objetos de dominio, eso es un retroceso al registro activo, simplemente un error cuando el servicio intenta guardar.

La regla de negocio no es "No pueden existir dos instancias de Usuario con el mismo SocialSecurityNumber al mismo tiempo"

Es que no pueden existir en el mismo repositorio.

3: No es difícil escribir repositorios en lugar de métodos de actualización de propiedades individuales. De hecho, encontrará que tiene prácticamente el mismo código en ambos sentidos. Es solo en qué clase lo pones.

Los ORM en estos días son fáciles y no tienen restricciones adicionales en su código. Habiendo dicho eso, personalmente prefiero simplemente arrancar el SQL a mano. No es tan difícil, nunca te encuentras con ningún problema con las funciones de ORM y puedes optimizar donde sea necesario.

Realmente no hay necesidad de realizar un seguimiento de qué propiedades cambiaron cuando guardó. Mantenga sus objetos de dominio pequeños y simplemente sobrescriba la versión anterior.

Preguntas generales

  1. La lógica de transacción va en el repositorio. Pero no deberías tener mucho si es que lo tienes. Claro que necesita algunos si tiene tablas secundarias en las que coloca los objetos secundarios del agregado, pero eso estará completamente encapsulado dentro del método de repositorio SaveMyObject.

  2. Actualizaciones masivas. Sí, debe modificar individualmente cada objeto y luego agregar un método SaveMyObjects (Lista de objetos) a su repositorio, para realizar la actualización masiva.

    Desea que el Objeto de dominio o el Servicio de dominio contengan la lógica. No es la base de datos. Eso significa que no puede simplemente "actualizar el nombre del conjunto de clientes = x donde y", porque, por lo que sabe, el objeto Cliente o CustomerUpdateService hace otras 20 cosas extrañas cuando cambia el nombre.

Ewan
fuente
Gran respuesta. Tiene toda la razón, estoy acostumbrado a un estilo de codificación de registro activo, por lo que el patrón de repositorio parece extraño a primera vista. Sin embargo, ¿los objetos de dominio "lean" (en AddressIdlugar de Address) contradicen los principios OO?
Doble M
no, todavía tienes un objeto Address, simplemente no es hijo del Cliente
Ewan
mapeo de objetos publicitarios sin seguimiento de cambios softwareengineering.stackexchange.com/questions/380274/…
Doble M
2

Respuesta corta: su comprensión es correcta y las preguntas que tiene apuntan a problemas válidos para los cuales las soluciones no son directas ni aceptadas universalmente.

Punto 2 .: (cargando gráficos de objetos completos)

No soy el primero en señalar que los ORM no siempre son una buena solución. El principal problema es que los ORM no saben nada sobre el caso de uso real, por lo que no tienen idea de qué cargar o cómo optimizar. Esto es un problema.

Como dijiste, la solución obvia es tener métodos de persistencia para cada caso de uso. Pero si todavía usa un ORM para eso, el ORM lo obligará a empaquetar todo en objetos de datos. Lo que, aparte de no estar realmente orientado a objetos, tampoco es el mejor diseño para algunos casos de uso.

¿Qué sucede si solo quiero actualizar en masa algunos registros? ¿Por qué necesitaría una representación de objeto para todos los registros? Etc.

Entonces, la solución a esto es simplemente no usar un ORM para casos de uso para los cuales no es una buena opción. Implemente un caso de uso "naturalmente" tal como es, que a veces no requiere una "abstracción" adicional de los datos en sí (objetos de datos) ni una abstracción sobre las "tablas" (repositorios).

Tener objetos de datos a medio llenar o reemplazar referencias de objetos con "identificadores" son, en el mejor de los casos, soluciones alternativas, no buenos diseños, como usted señaló.

Punto 3 .: (comprobando restricciones)

Si la persistencia no se abstrae, cada caso de uso obviamente puede verificar cualquier restricción que desee fácilmente. El requisito de que los objetos no conozcan el "repositorio" es completamente artificial y no es un problema de la tecnología.

Punto 5 .: (ORM)

Es, por supuesto, un requisito previo esencial para desacoplar la implementación de almacenamiento concreto del código de dominio. Sin embargo, ¿realmente tiene un costo tan alto?

No lo hace Hay muchas otras formas de tener persistencia. El problema es que el ORM es visto como "la" solución a usar, siempre (al menos para bases de datos relacionales). Intentar sugerir no usarlo para algunos casos de uso en un proyecto es inútil y, dependiendo del ORM en sí, a veces incluso imposible, ya que estas herramientas a veces usan cachés y ejecución tardía.

Pregunta general 1 .: (transacciones)

No creo que haya una única solución. Si su diseño está orientado a objetos, habrá un método "superior" para cada caso de uso. La transacción debería estar allí.

Cualquier otra restricción es completamente artificial.

Pregunta general 2 .: (operaciones a granel)

Con un ORM, usted (para la mayoría de los ORM que conozco) se ve obligado a atravesar objetos individuales. Esto es completamente innecesario y probablemente no sería su diseño si el ORM no atara su mano.

El requisito de separar la "lógica" de SQL proviene de los ORM. Ellos tienen que decir que, ya que no pueden apoyarlo. No es inherentemente "malo".

Resumen

Supongo que mi punto es que los ORM no siempre son la mejor herramienta para un proyecto, e incluso si lo es, es muy poco probable que sea el mejor para todos los casos de uso en un proyecto.

Del mismo modo, la abstracción del repositorio de objetos de datos de DDD tampoco es siempre la mejor. Incluso iría tan lejos como para decir que rara vez son el diseño óptimo.

Eso nos deja sin una solución única para todos, por lo que tendríamos que pensar en soluciones para cada caso de uso individualmente, lo cual no es una buena noticia y obviamente hace que nuestro trabajo sea más difícil :)

Robert Bräutigam
fuente
Puntos muy interesantes allí, gracias por confirmar mis suposiciones. Dijiste que hay muchas otras formas de tener persistencia. ¿Puede recomendar un patrón de diseño eficaz para usar con bases de datos de gráficos (sin ORM), que aún proporcionaría PI?
Doble M
1
En realidad, me preguntaría si necesita aislamiento (y de qué tipo) en primer lugar. Aislar por tecnología (es decir, base de datos, interfaz de usuario, etc.) trae casi automáticamente la "incomodidad" que está tratando de evitar, para la ventaja de un reemplazo algo más fácil de la tecnología de la base de datos. Sin embargo, el costo es un cambio más difícil de la lógica de negocios, ya que se extiende a través de las capas. O puede dividirse entre las funciones empresariales, lo que dificultaría el cambio de las bases de datos, pero el cambio de la lógica será más fácil. ¿Cuál quieres realmente?
Robert Bräutigam
1
Puede obtener el mejor rendimiento si solo modela el dominio (es decir, las funciones empresariales) y no abstrae la base de datos (ya sea que el relacional o el gráfico no importen). Dado que la base de datos no se abstrae del caso de uso, el caso de uso puede implementar las consultas / actualizaciones más óptimas que desea, y no necesita pasar por algún modelo de objeto incómodo para lograr lo que quiere.
Robert Bräutigam
Bueno, el objetivo principal es mantener las preocupaciones de persistencia alejadas de la lógica comercial, para tener un código limpio que sea fácil de entender, expandir y probar. Poder intercambiar tecnologías DB es solo una ventaja. Puedo ver que obviamente existe fricción entre la eficiencia y la ignorancia, lo que parece ser más fuerte con los DB de gráficos debido a las poderosas consultas que puede (pero no se les permite) usar.
Doble M
1
Como desarrollador de Java Enterprise, puedo decirle que hemos tratado de separar la persistencia de la lógica durante las últimas dos décadas. No funciona Primero, la separación nunca se logró realmente. Incluso hoy en día, hay todo tipo de cosas relacionadas con la base de datos en objetos supuestamente "comerciales", el principal es la identificación de la base de datos (y muchas anotaciones de la base de datos). En segundo lugar, como dijiste, a veces la lógica de negocios se ejecuta en la base de datos de cualquier manera. Tercero, esa es la razón por la que tenemos bases de datos específicas, para poder descargar algo de lógica mejor donde están los datos.
Robert Bräutigam