asp.net-mvc - español - asp.net core mvc
Renunció a DDD, pero necesita algunos de sus beneficios (5)
Renuncio a la DDD tradicional, que a menudo es un gran maestro del tiempo, y me obliga a hacer un mapeo interminable: data layer <--> domain layer <--> presentation layer
.
Incluso para un pequeño cambio, debo cambiar los modelos de datos, los modelos de dominio, los modelos de presentación / modelos de vista, luego los repositorios, las clases de administrador / servicio y, por supuesto, los mapas de AutoMapper, ¡y luego probar todo! Cada llamada requiere llamar a una capa que llama a una capa que llama al código subyacente. Y no obtengo nada a cambio más que "es posible que lo necesite en el futuro" . Meh
Mi enfoque actual es más pragmático:
- Ya no me preocupo por la diferencia entre la "capa de datos" y la "capa de dominio", ya que no tiene sentido, los términos son intercambiables. Dejo que EF haga su trabajo y agrego interfaces y repositorios en la parte superior cuando sea necesario.
- He fusionado mis proyectos de "datos" y "dominio" (en "núcleo", nombre aburrido, lo sé), y casi podría jurar que Visual Studio se está ejecutando más rápido.
- Permito que las entidades EF suban y bajen de la pila, pero aún así las asigno a modelos de presentación / modelos de vista como de costumbre.
- Para operaciones simples, llamo a los repositorios directamente desde los controladores, para operaciones complejas, uso los administradores / servicios de dominio como de costumbre; Los repositorios nunca exponen IQueryable.
- Defino entidades / POCO como clases parciales, por lo que puedo agregar el comportamiento del dominio por separado en las clases parciales correspondientes.
El problema: ahora uso las entidades en todo el lugar, por lo que el código del cliente puede ver sus propiedades de navegación . Y los modelos siempre se materializan después de salir de un repositorio, por lo que las propiedades de navegación suelen ser nulas.
Soluciones posibles:
1. Vive con ello. Es feo pero preferible a los problemas explicados anteriormente.
2. Para cada entidad, defina una interfaz que oculte las propiedades de navegación; y hacer que el código del cliente use las interfaces. Pero, irónicamente, esto significa otra capa (aunque delgada y manejable).
3. ¿Qué más?
No estoy acostumbrado a este tipo de estilo de programación rápido y suelto, así que tal vez me esté perdiendo algunos trucos obvios. ¿Hay algo más que deba tener en cuenta? Estoy seguro de que hay otros problemas que encontraré pronto.
EDITAR: Esta pregunta no es sobre DDD. Y tenga en cuenta que muchos luchan con un enfoque tradicional de DDD: Seemann parece llegar a la misma conclusión , Rahien habla sobre la "Abstracción inútil por el patrón de abstracción anti" , y el propio Evans dijo que el DDD solo es verdaderamente útil en el 5% de casos. También vea este hilo . Algunos de los comentarios / respuestas son, de manera predecible, acerca de cómo estoy haciendo mal el DDD, o cómo puedo modificar mi sistema para hacerlo correctamente. Sin embargo, no estoy preguntando por DDD ni por descifrarlo en los casos en que sea adecuado, sino que me gustaría saber qué están haciendo los demás en línea con el pensamiento que he descrito anteriormente. No es que DDD sea una panacea para todos los problemas de diseño, cada década se produce un nuevo proceso (RUP anyone? XP, Agile, Booch, blah ...). DDD es simplemente la más brillante, y la más conocida y utilizada. Pero el pragmatismo debería ser lo primero, ya que estoy tratando de construir productos vendibles que se envíen a tiempo y sean fáciles de mantener. El axioma de programación más útil que he aprendido, con diferencia , es YAGNI. Lo que quiero es cambiar mi sistema a una especie de "DDD-lite", donde obtengo su sólida filosofía de diseño / OOP / patrón, pero sin la grasa.
El problema: ahora uso las entidades en todo el lugar, por lo que el código del cliente puede ver sus propiedades de navegación.
No entiendo bien por qué esto es un problema y cómo se relaciona con las entidades de EF en particular. Por código de cliente, ¿quiere decir código de capa de presentación o cualquier código que consuma sus entidades?
Para el código de UI, una solución simple es definir ViewModels que simplemente no exponen estas propiedades de navegación (o solo exponen algunas de ellas en función de la profundidad del gráfico de objetos que sus GUIs necesitan).
Para otro código, es normal ver las propiedades de navegación de las entidades. Son públicos por una razón. Puedes terminar rompiendo la Ley de Deméter si abusas de ellos, pero es una cuestión de disciplina del desarrollador no caer en esa trampa.
Una entidad contiene su propio contrato: se supone que todo el código que tiene acceso a la entidad puede utilizar cualquier parte de este contrato. Si sientes que tus entidades están exponiendo demasiado y que necesitas poner interfaces encima de ellas para restringir el acceso a ciertas partes, tal vez sea solo una entidad diferente.
- Ya no me preocupa la diferencia entre la "capa de datos" y la "capa de dominio", ya que no tiene sentido, los términos son
intercambiable. Dejo que EF haga lo suyo, y añado interfaces y
repositorios en la parte superior cuando sea necesario.- He fusionado mis proyectos de "datos" y "dominio" (en "núcleo", nombre aburrido, lo sé), y casi podría jurar que Visual Studio es
en realidad correr más rápido.- Permito que las entidades EF suban y bajen de la pila, pero aún así las asigno a modelos de presentación / modelos de vista como de costumbre.
- Para operaciones simples, llamo a los repositorios directamente desde los controladores, para operaciones complejas, uso los administradores / servicios de dominio como de costumbre; Los repositorios nunca exponen IQueryable.
- Defino entidades / POCO como clases parciales, por lo que puedo agregar el comportamiento del dominio por separado en las clases parciales correspondientes.
Ninguna de estas cosas parece ser fundamentalmente anti-DDD para mí, excepto la separación de datos / dominio.
Especialmente si lo hace la base de datos, EF -DDD es claramente un enfoque centrado en el dominio y no debe definir sus tablas antes de definir sus entidades. Tampoco está claro si algunas de las entidades de su dominio se comunican directamente con la base de datos o con EF (no con DDD, y, en general, con arquitectura de capas), o si tiene de manera sistemática objetos de acceso a los datos en el medio (compatible con DDD).
¿Cómo puede alguien darle buenos consejos cuando no tenemos idea de qué es lo que está construyendo? En el gran esquema de las cosas, podrías estar construyendo la solución equivocada (sin decir que lo eres). Entonces, comprenda que todo lo que podemos relacionar es con problemas de diseño técnico y experiencias pasadas similares.
Muchas personas se enfrentan a su problema, de hecho. El mapeo es impuesto de acoplamiento suelto en el terreno de la tipificación estática. Tal vez un lenguaje más dinámico podría resolver algo de tu dolor. O tal vez podría encontrar virtud en la automatización de más (DSL, MDA). También podría cambiar al servidor cliente en su lugar.
Las interfaces no son capas, sino abstracciones. Úsalos sabiamente.
Personalmente, nunca tomaría estos atajos. Ha sido mordido demasiadas veces intentando saltar pasos. La lógica comienza a aparecer en lugares extraños. Si tengo una aplicación basada en datos para desarrollar conjuntos de datos simples, también me viene a la mente, EF. Pero no llamo a los objetos agregado o entidad en el sentido de DDD, solo entidad en el sentido de ERD. Transactionscript podría ser un mejor ajuste que hacer el método parcial de aspersión. En cuanto a los objetos del modelo de lectura, estos no son capas de direccionamiento indirecto.
En general, tengo la sensación, y es solo que, estás haciendo un montón de cosas porque luchas contra la fricción del mapeo al tomar una dependencia de los objetos que no revelan la forma requerida (propiedades de navegación que son nulas) por lo tanto causando problemas en un área diferente.
Algunas reflexiones sobre este punto:
... los repositorios nunca exponen IQueryable ... los modelos siempre se materializan después de dejar un repositorio ...
Su pregunta está etiquetada con "asp.net-mvc", por lo que tiene una aplicación web en mente. El 90% o más de todas las solicitudes serán solicitudes GET que se supone que recuperan algunos datos de la base de datos y muestran esos datos en una vista web. ¿Con qué frecuencia esos datos necesarios son realmente entidades en lugar de solo bolsas de propiedades (una selección de propiedades de un tipo de entidad o quizás compuesto de propiedades de múltiples entidades)?
Diga, su aplicación tiene 100 vistas. Solo una minoría de estas mostrará entidades completas:
- 50 de ellas son vistas de lista que muestran datos seleccionados (un cliente con ID y dirección, pero sin la persona de contacto, número de teléfono y volumen de ventas del cliente)
- 20 de ellos contienen cuadros de texto de autocompletado para seleccionar una referencia (el cliente para un pedido, pero solo se muestra el nombre y la ciudad del cliente en la lista de autocompletar, no el resto de la dirección ni la persona de contacto, el número de teléfono y el volumen de ventas, y solo el Se muestran los primeros 5 hits)
- 1 es una vista de edición para un cliente que muestra todo, pero no el volumen de ventas
- 1 es una vista de detalles para un cliente con sus últimos cinco pedidos
- 1 es una vista detallada de un pedido que incluye artículos de pedido, incluido el producto de cada artículo pero sin el nombre del proveedor del producto
- 1 es la misma vista pero especializada para el departamento de compras que desea ver al proveedor para cada artículo y el producto del artículo con el tiempo de entrega promedio del proveedor durante los últimos tres meses.
- 1 es una vista para el departamento de servicio que muestra el pedido con solo los artículos de pedido de la categoría de producto "servicio de reparación"
- 1 vista para el departamento de Recursos Humanos muestra a los empleados, incluida una foto almacenada como un gran blob
- 1 vista para el departamento de planificación de personal muestra una versión corta del empleado sin foto
- etcétera etcétera.
Como programador de UI, tendría todo tipo de requisitos de datos para mostrar una vista con los ejemplos anteriores:
- Sólo necesito una selección de propiedades
- Necesito incluso diferentes selecciones de las propiedades de la misma entidad para diferentes vistas
- Necesito un pedido que incluya todos los artículos pero sin una referencia a un producto
- Necesito un pedido que incluya todos los artículos (pero no todas las propiedades de los artículos) e incluya una referencia a un producto y a un proveedor (pero no a todas las propiedades del proveedor)
- Necesito un pedido que incluya solo una lista filtrada de artículos de pedido
- Necesito un cliente que incluya los últimos cinco pedidos, no los 3000 pedidos que tuvo
- Necesito un empleado pero por favor sin la gran imagen de blob
- etcétera etcétera.
¿Cómo cumplir estos requisitos como desarrollador de servicios / repositorio / servicio de datos?
- Solo proporciono un puñado de métodos y materializo las entidades: encabezado de orden de carga, encabezado de orden de carga con artículos, encabezado de orden de carga con artículos y producto, encabezado de orden de carga con artículos y proveedor, encabezado de cliente de carga (tirar 15 de las 20 propiedades) , estimado desarrollador de IU, si solo necesita cinco propiedades), cargue el encabezado del cliente con los 3000 pedidos (elimine 2995, estimado desarrollador de IU, si solo necesita cinco), etc., etc. Devuelvo interfaces de los repositorios que no se ocultan. Propiedades de navegación cargadas.
- Me interesan todos los detalles que necesita la interfaz de usuario: creo los métodos de repositorio / servicio como
GetFiveCustomerPropertiesForAutoComplete
,GetCustomerWithLastFiveOrders
, etc. Regreso a las interfaces de los repositorios que ocultan las propiedades (también escalar) que no he cargado. O devuelvo "DTO" que contienen las propiedades solicitadas. Cambio el repositorio / servicios y creo nuevos DTO todos los días cuando un desarrollador de IU llama con un requisito de datos para la siguiente vista. -
IQueryable<TEntity>
de los repositorios y le digo al desarrollador de la interfaz de usuario "cree usted mismo la consulta LINQ para obtener los datos que necesita para sus vistas". (A la mañana siguiente, el DBA se está quejando de cientos de consultas de bases de datos de rendimiento terrible). -
IQueryable<TEntity>
s "preparadas" de los repositorios / servicios que cubren, por ejemplo, problemas de seguridad como la aplicación de cláusulasWhere
para los derechos de acceso del usuario o adjunto una cláusulaWhere
para un término de búsqueda o aplico una opciónNoTracking
a la consulta. Le digo al desarrollador de la interfaz de usuario: "Se le permite extender la consulta con a) proyecciones (Select
), b) paginación (Take
ySkip
) y quizás c) ordenar (OrderBy
) porque considero que esas tres partes de la consulta son de UI. otros requisitos de consulta (filtrado, unión, agrupación, etc.) deben implementarse en la capa de servicio / repositorio y están prohibidos en la capa de interfaz de usuario ". La pieza más importante aquí son las proyecciones que materializan ViewModels directamente a través de la consulta LINQ / SQL sin capa de mapeo intermedia y sin la sobrecarga para cargar más que las columnas / propiedades necesarias.
Estos son sólo algunos pensamientos. Cada enfoque tiene sus beneficios y desventajas. Trabajando en equipos pequeños donde al menos uno o unos pocos desarrolladores tienen una visión general de lo que está sucediendo tanto en el repositorio / servicio como en la capa UI / "proyección", la última opción funciona bien para mí en mi experiencia, aunque no siempre funciona con las reglas estrictas descritas (por ejemplo, el filtro por categoría de producto para los artículos incluidos en el pedido de un pedido requiere aplicar una cláusula Where
dentro de la proyección, es decir, en la capa UI). Para solicitudes POST y modificaciones de datos, usaría DTO que envían datos recopilados de una vista a un servicio que se procesará allí.
Para una separación más estricta de "capa de consulta" y capa de interfaz de usuario, probablemente preferiría algo similar a la segunda opción, tal vez no con una interfaz / DTO para cada requisito de UI, pero de alguna manera reducido a un conjunto de DTO para los requisitos más comunes (con el precio de una pequeña sobrecarga de propiedades cargadas a veces innecesariamente). Sin embargo, espero que sea más trabajo que la última opción debido a la mayor cantidad de métodos necesarios de repositorio / servicio, el mantenimiento adicional de (quizás muchos) DTO y la asignación intermedia entre DTO y ViewModels.
Personalmente, me preocupa materializar entidades completas, especialmente gráficos de objetos complejos, cuando no los necesito el 90% del tiempo. Pero mi preocupación no se verifica mediante mediciones de rendimiento exhaustivas, lo que demuestra que este enfoque es realmente un problema para una aplicación "normal" que no tiene necesidades especiales de alto rendimiento.
Intentaré ser breve: optamos por el método 2, es decir, agregamos una capa de interfaces que utiliza en el cliente. Puede hacer que EF los genere por usted, solo un pequeño ajuste de las plantillas .tt.
Sí, crea (todavía) otra capa, pero no tiene lógica y no agrega complejidad. Por supuesto, si su cliente necesita deserializar entidades, debe agregar (aún) otra capa que maneje la deserialización y haga referencia tanto a las definiciones de entidades como a las interfaces que devolverá al cliente. Pero también es delgado, así que aprendimos a vivir con él, porque funcionó bien y el cliente realmente se mantiene limpio ...
Un enfoque de persistencia típico con DDD es mapear el modelo de dominio directamente a las tablas correspondientes. Técnicamente, las asignaciones todavía están allí (y generalmente se declaran en código), pero no hay un modelo de datos explícito, como lo señala lazyberezovsky .
El problema con las propiedades de navegación se puede resolver de diferentes maneras, independientemente de si está utilizando DDD o no. No me gusta el enfoque 1 porque hace que sea más difícil razonar acerca de su código: nunca se sabe qué propiedades se establecerán y cuáles no. El enfoque 2 es mucho mejor en teoría, porque lo hace muy explícito lo que requiere una consulta dada y hacer las cosas explícitas es una buena práctica en general. Un enfoque similar, pero más simple y menos quebradizo es usar read-models , que son solo objetos diseñados para cumplir con los requisitos de una consulta determinada de un conjunto de consultas. En el contexto de DDD, le permiten separar las entidades ricas en comportamiento de las consultas, que a menudo están en desacuerdo. Ahora los defensores de DRY pueden gritar una herejía y atacarte con antorchas y horcas, pero en la práctica a menudo es mucho más fácil mantener un modelo de lectura y una entidad que intentar coaccionar a las entidades para que cumplan los requisitos de consulta mediante interfaces o mapeo complejo. estrategias. Además, las responsabilidades de un modelo de lectura y un modelo de comportamiento son bastante diferentes, por lo tanto, DRY no es aplicable.
Esto no quiere decir que DDD sea aplicable en su escenario. A menudo, es una decisión acertada para evitar la DDD completa, especialmente en escenarios que son en su mayoría CRUD . Tienes razón al ser cauteloso, un buen ejemplo de KISS y YAGNI . DDD obtiene beneficios cuando su dominio consiste en un comportamiento complejo, no solo en datos. En cualquier caso, se aplica el patrón de lectura-modelo.
ACTUALIZAR
Para implementaciones que no emplean un modelo de lectura, eche un vistazo a Fetching Strategy Design, donde la noción de una estrategia de recuperación permite especificar exactamente lo que se necesita de la base de datos que mitiga los problemas con las propiedades de navegación. El material al que se hace referencia en el post vinculado también es de interés. En general, esto intenta evitar la capa de direccionamiento indirecto presente en otros enfoques. Sin embargo, en mi opinión, usar la estrategia de recuperación propuesta es más complejo que usar un modelo de lectura mientras que el resultado neto es el mismo.