Tenemos una API web ASP.NET que proporciona una API REST para nuestra aplicación de página única. Usamos DTO / POCO para pasar datos a través de esta API.
El problema ahora es que estos DTO se hacen más grandes con el tiempo, así que ahora queremos refactorizar los DTO.
Estoy buscando "mejores prácticas" para diseñar un DTO: actualmente tenemos pequeños DTO que consisten solo en campos de tipo de valor, por ejemplo:
public class UserDto
{
public int Id { get; set; }
public string Name { get; set; }
}
Otros DTO utilizan este UserDto por composición, por ejemplo:
public class TaskDto
{
public int Id { get; set; }
public UserDto AssignedTo { get; set; }
}
Además, hay algunos DTO extendidos que se definen heredando de otros, por ejemplo:
public class TaskDetailDto : TaskDto
{
// some more fields
}
Dado que algunos DTO se han utilizado para varios puntos finales / métodos (por ejemplo, GET y PUT), algunos campos los han ampliado de forma incremental a lo largo del tiempo. Y debido a la herencia y la composición, otros DTO también se hicieron más grandes.
Mi pregunta ahora es si la herencia y la composición no son buenas prácticas. Pero cuando no los reutilizamos, se siente como escribir el mismo código varias veces. ¿Es una mala práctica usar un DTO para múltiples puntos finales / métodos, o debería haber diferentes DTO, que solo difieren en algunos matices?
Respuestas:
Como práctica recomendada, intente que sus DTO sean lo más concisos posible. Solo devuelva lo que necesita devolver. Solo use lo que necesita usar. Si eso significa algunos DTO adicionales, que así sea.
En su ejemplo, una tarea contiene un usuario. Uno probablemente no necesita un objeto de usuario completo allí, tal vez solo el nombre del usuario que tiene asignada la tarea. No necesitamos el resto de las propiedades del usuario.
Supongamos que desea reasignar una tarea a un usuario diferente, uno puede verse tentado a pasar un objeto de usuario completo y un objeto de tarea completa durante la publicación de reasignación. Pero lo que realmente se necesita es solo el ID de la tarea y el ID del usuario. Esas son las dos únicas piezas de información necesarias para reasignar una tarea, así que modele el DTO como tal. Esto generalmente significa que hay un DTO separado para cada llamada de descanso si se esfuerza por un modelo DTO delgado.
Además, a veces uno puede necesitar herencia / composición. Digamos que uno tiene un trabajo. Un trabajo tiene múltiples tareas. En ese caso, obtener un trabajo también puede devolver la lista de tareas para el trabajo también. Entonces, no hay una regla contra la composición. Depende de lo que se esté modelando.
fuente
A menos que su sistema se base estrictamente en operaciones CRUD, sus DTO son demasiado granulares. Intente crear puntos finales que incorporen procesos comerciales o artefactos. Este enfoque se adapta muy bien a una capa de lógica de negocios y al "diseño impulsado por dominio" de Eric Evans.
Por ejemplo, supongamos que tiene un punto final que devuelve datos para una factura para que pueda mostrarse en una pantalla o formulario al usuario final. En un modelo CRUD, necesitaría varias llamadas a sus puntos finales para reunir la información necesaria: nombre, dirección de facturación, dirección de envío, líneas de pedido. En un contexto de transacción comercial, un único DTO de un único punto final puede devolver toda esta información a la vez.
fuente
Es difícil establecer las mejores prácticas para algo tan "flexible" o abstracto como un DTO. Esencialmente, los DTO son solo objetos para la transferencia de datos, pero dependiendo del destino o el motivo de la transferencia, es posible que desee aplicar diferentes "mejores prácticas".
Recomiendo leer Patrones de arquitectura de aplicaciones empresariales de Martin Fowler . Hay un capítulo completo dedicado a los patrones, donde los DTO obtienen una sección realmente detallada.
Originalmente, fueron "diseñados" para ser usados en costosas llamadas remotas, donde probablemente necesitaría muchos datos de diferentes partes de su lógica; Los DTO harían la transferencia de datos en una sola llamada.
Según el autor, los DTO no estaban destinados a ser utilizados en entornos locales, pero algunas personas encontraron un uso para ellos. Por lo general, se utilizan para recopilar información de diferentes POCO en una sola entidad para GUI, API o diferentes capas.
Ahora, con la herencia, la reutilización del código es más un efecto secundario de la herencia que su objetivo principal; La composición, por otro lado, se implementa con la reutilización del código como objetivo principal.
Algunas personas recomiendan el uso de la composición y la herencia juntos, utilizando las fortalezas de ambos y tratando de mitigar sus debilidades. Lo siguiente es parte de mi proceso mental al elegir o crear nuevos DTO, o cualquier clase / objeto nuevo para el caso:
Algunos de ellos tal vez no sean las "mejores" prácticas, funcionan bastante bien para los proyectos en los que he estado trabajando, pero debe recordar que no hay tamaño para todos. En el caso del DTO universal, debe tener cuidado, las firmas de mis métodos se ven así:
Si alguno de los métodos alguna vez necesita su propio DTO, heredo y, por lo general, el único cambio que necesito hacer es el parámetro, aunque a veces necesito profundizar en casos específicos.
De sus comentarios, entiendo que está utilizando DTO anidados. Si sus DTO anidados consisten solo en una lista de otros DTO, creo que lo mejor es desenvolver la lista.
Dependiendo de la cantidad de datos que necesite mostrar o trabajar, puede ser una buena idea crear nuevos DTO que limiten los datos; por ejemplo, si su UserDTO tiene muchos campos y solo necesita 1 o 2, puede ser mejor tener un DTO con solo esos campos. Definir la capa, el contexto, el uso y la utilidad de un DTO ayudará mucho al diseñarlo.
fuente
Usar composición en DTO es una práctica perfectamente buena.
La herencia entre tipos concretos de DTO es una mala práctica.
Por un lado, en un lenguaje como C #, una propiedad implementada automáticamente tiene muy poca sobrecarga de mantenimiento, por lo que duplicarlas (realmente aborrezco la duplicación) no es tan perjudicial como a menudo lo es.
Una razón para no utilizar la herencia concreta entre los DTO es que ciertas herramientas los asignarán felizmente a los tipos incorrectos.
Por ejemplo, si usa una utilidad de base de datos como Dapper (no lo recomiendo pero es popular) que realiza
class name -> table name
inferencia, podría terminar guardando fácilmente un tipo derivado como un tipo base concreto en algún lugar de la jerarquía y, por lo tanto, perder datos o peor .Una razón más profunda para no usar la herencia entre los DTO es que no debe usarse para compartir implementaciones entre tipos que no tienen una relación obvia "es una". En mi opinión,
TaskDetail
no suena como un subtipo deTask
. Podría ser fácilmente una propiedad deTask
o, peor aún, podría ser un supertipo deTask
.Ahora, una cosa que puede preocuparle es mantener la coherencia entre los nombres y tipos de las propiedades de varios DTO relacionados.
Si bien la herencia de tipos concretos ayudaría, como consecuencia, a garantizar dicha coherencia, es mucho mejor utilizar interfaces (o clases base virtuales puras en C ++) para mantener ese tipo de coherencia.
Considere los siguientes DTO
fuente