Usar composición y herencia para DTO

13

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?

oficial
fuente
66
No puedo decirte qué debes hacer, pero según mi experiencia trabajando con DTO, la herencia y la composición más temprano que tarde te cazan como un mal café. Nunca volveré a usar DTO de nuevo. Nunca. Además, no considero que los DTO similares sean una violación SECA. Dos puntos finales que devuelven la misma representación pueden reutilizar los mismos DTO. Dos puntos finales que devuelven representaciones similares no devuelven los mismos DTO, por lo que hago DTO específicos para cada uno . Si tuviera que elegir, la composición es la menos problemática a largo plazo.
Laiv
@Laiv esta es la respuesta correcta a la pregunta, simplemente no lo hagas. No estoy seguro de por qué lo pones como un comentario
TheCatWhisperer
2
@Laiv: ¿Qué utilizas en su lugar? En mi experiencia, las personas que luchan con esto simplemente lo están pensando demasiado. Un DTO es solo un contenedor de datos, y eso es todo.
Robert Harvey
TheCatWhisperer porque mis argumentos se basarían principalmente en la opinión. Todavía estoy tratando de abordar este tipo de problemas en mis proyectos. @RobertHarvey es cierto, no sé por qué tiendo a ver las cosas más difíciles de lo que realmente son. Todavía estoy trabajando en la solución. Estaba bastante convencido de que HAL era el modelo destinado a resolver estos problemas, pero al leer su respuesta, me di cuenta de que yo también hago mi DTO demasiado granular. Así que pondré en práctica su enfoque primero. Los cambios serán menos dramáticos que cambiar completamente a HATEOAS.
Laiv
Pruebe esto para una buena discusión que podría ayudarlo: stackoverflow.com/questions/6297322/…
johnny

Respuestas:

9

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.

Jon Raynor
fuente
Lo más conciso posible también significa que debería recrear DTO si solo difieren en algunos detalles, ¿no?
oficial
1
@officer - En general, sí.
Jon Raynor
2

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.

Robert Harvey
fuente
Robert, ¿quieres decir con una raíz agregada aquí?
johnny
Actualmente, nuestros DTO están diseñados para proporcionar todos los datos de un formulario, por ejemplo, cuando se muestra una lista de tareas, TodoListDTO tiene una Lista de TaskDTO. Pero como estamos reutilizando el TaskDTO, cada uno de ellos también tiene un UserDTO. Entonces, el problema es la gran cantidad de datos que se consultan en el backend y se envían por cable.
oficial
¿Se requieren todos los datos?
Robert Harvey
1

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:

  • Utilizo la herencia con DTO dentro de la misma capa o el mismo contexto. Un DTO nunca heredará de un POCO, un BLL DTO nunca heredará de un DAL DTO, etc.
  • Si me encuentro tratando de ocultar un campo de un DTO, refactorizaré y tal vez use composición en su lugar.
  • Si muy pocos campos diferentes de un DTO base es todo lo que necesito, los pondré en un DTO universal. Los DTO universales se usan solo internamente.
  • Un POCO / DTO base casi nunca se usará para ninguna lógica, de esa manera la base responde solo a las necesidades de sus hijos. Si alguna vez necesito usar la base, evito agregar cualquier campo nuevo que sus hijos nunca usen.

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

public void DoSomething(BaseDTO base) {
    //Some code 
}

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.

IvanGrasp
fuente
No pude agregar más de 2 enlaces en mi respuesta, aquí hay más información sobre los DTO locales. Soy consciente de que es información muy antigua, pero creo que parte de ella puede ser relevante.
IvanGrasp
1

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 nameinferencia, 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, TaskDetailno suena como un subtipo de Task. Podría ser fácilmente una propiedad de Tasko, peor aún, podría ser un supertipo de Task.

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

interface IIdentity
{
    int Id { get; set; }
}

interface INamed
{
    string Name { get; set; }
}

public class UserDto: IIdentity, INamed
{
    public int Id { get; set; }

    public string Name { get; set; }

    // User specific properties
}

public class TaskDto: IIdentity
{
    public int Id { get; set; }

    // Task specific properties
}
Aluan Haddad
fuente