Utiliser la composition et l'héritage pour les DTO

13

Nous avons une API Web ASP.NET qui fournit une API REST pour notre application de page unique. Nous utilisons des DTO / POCO pour transmettre des données via cette API.

Le problème est maintenant que ces DTO grossissent avec le temps, alors maintenant nous voulons refactoriser les DTO.

Je recherche des "meilleures pratiques" pour concevoir un DTO: Actuellement, nous avons de petits DTO qui ne sont composés que de champs de type valeur, par exemple:

public class UserDto
{
    public int Id { get; set; }

    public string Name { get; set; }
}

D'autres DTO utilisent ce UserDto par composition, par exemple:

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

    public UserDto AssignedTo { get; set; }
}

En outre, certains DTO étendus sont définis en héritant des autres, par exemple:

public class TaskDetailDto : TaskDto
{
    // some more fields
}

Étant donné que certains DTO ont été utilisés pour plusieurs points d'extrémité / méthodes (par exemple, GET et PUT), ils ont été étendus de manière incrémentielle de certains champs au fil du temps. Et en raison de l'héritage et de la composition, d'autres DTO sont également devenus plus importants.

Ma question est maintenant: l'héritage et la composition ne sont-ils pas de bonnes pratiques? Mais quand on ne les réutilise pas, on a l'impression d'écrire plusieurs fois le même code. Est-ce une mauvaise pratique d'utiliser un DTO pour plusieurs points de terminaison / méthodes, ou devrait-il y avoir différents DTO, qui ne diffèrent que par certaines nuances?

officier
la source
6
Je ne peux pas vous dire ce que vous devez faire, mais d'après mon expérience de travail avec le DTO, l'héritage et la composition vous chassent plus tôt que tard comme un mauvais café. Je ne réutiliserai plus jamais DTO. Déjà. De plus, je ne considère pas les DTO similaires comme une violation DRY. Deux points de terminaison qui renvoient la même représentation peuvent réutiliser les mêmes DTO. Deux points de terminaison qui renvoient des représentations similaires ne renvoient pas les mêmes DTO, donc je crée des DTO spécifiques pour chacun . Si j'étais obligé de choisir, la composition est la moins problématique à long terme.
Laiv
@Laiv c'est la bonne réponse à la question, mais ne le faites pas. Je ne sais pas pourquoi vous l'avez mis en commentaire
TheCatWhisperer
2
@Laiv: Qu'utilisez-vous à la place? D'après mon expérience, les gens qui luttent avec cela sont tout simplement trop penser. Un DTO n'est qu'un conteneur de données, et c'est tout.
Robert Harvey
TheCatWhisperer parce que mes arguments seraient principalement basés sur l'opinion. J'essaie toujours de résoudre ce genre de problèmes dans mes projets. @RobertHarvey vrai, je ne sais pas pourquoi j'ai tendance à voir les choses plus difficiles qu'elles ne le sont vraiment. Je travaille toujours sur la solution. J'étais assez convaincu que HAL était le modèle destiné à résoudre ces problèmes, mais en lisant votre réponse, j'ai réalisé que je fais aussi mon DTO trop granulaire. Je vais donc mettre en pratique votre approche en premier. Les changements vont être moins spectaculaires que le passage complet à HATEOAS.
Laiv
Essayez ceci pour une bonne discussion qui pourrait vous aider: stackoverflow.com/questions/6297322/…
johnny

Réponses:

10

Comme meilleure pratique, essayez de rendre vos DTO aussi concis que possible. Ne retournez que ce dont vous avez besoin pour retourner. N'utilisez que ce dont vous avez besoin. Si cela signifie quelques DTO supplémentaires, tant pis.

Dans votre exemple, une tâche contient un utilisateur. On n'a probablement pas besoin d'un objet utilisateur complet à cet endroit, peut-être juste le nom de l'utilisateur auquel la tâche est attribuée. Nous n'avons pas besoin du reste des propriétés utilisateur.

Supposons que vous souhaitiez réaffecter une tâche à un autre utilisateur, on peut être tenté de transmettre un objet utilisateur complet et un objet de tâche complet lors de la publication de réaffectation. Mais, ce qui est vraiment nécessaire, c'est juste l'ID de tâche et l'ID utilisateur. Ce sont les deux seules informations nécessaires pour réaffecter une tâche, modélisez donc le DTO en tant que tel. Cela signifie généralement qu'il existe un DTO distinct pour chaque appel de repos si vous recherchez un modèle DTO allégé.

De plus, il peut parfois être nécessaire d'hériter / de composer. Disons que l'on a un travail. Un travail comporte plusieurs tâches. Dans ce cas, l'obtention d'un emploi peut également renvoyer la liste des tâches de l'emploi. Il n'y a donc pas de règle contre la composition. Cela dépend de ce qui est modélisé.

Jon Raynor
la source
Aussi concis que possible signifie également que je devrais recréer des DTO s'ils diffèrent simplement dans certains détails - n'est-ce pas?
officier
1
@officer - En général, oui.
Jon Raynor
2

À moins que votre système ne soit strictement basé sur des opérations CRUD, vos DTO sont trop granulaires. Essayez de créer des points de terminaison qui incarnent des processus métier ou des artefacts. Cette approche correspond bien à une couche de logique métier et à la «conception orientée domaine» d'Eric Evans.

Par exemple, supposons que vous ayez un point de terminaison qui renvoie des données pour une facture afin qu'elles puissent être affichées sur un écran ou un formulaire pour l'utilisateur final. Dans un modèle CRUD, vous auriez besoin de plusieurs appels vers vos points de terminaison pour rassembler les informations nécessaires: nom, adresse de facturation, adresse de livraison, éléments de campagne. Dans un contexte de transaction commerciale, un DTO unique à partir d'un seul point de terminaison peut renvoyer toutes ces informations à la fois.

Robert Harvey
la source
Robert, tu veux dire par une racine agrégée ici?
johnny
Actuellement, nos DTO sont conçus pour fournir toutes les données d'un formulaire, par exemple lors de l'affichage d'une liste de tâches, le TodoListDTO a une liste de TaskDTO. Mais puisque nous réutilisons le TaskDTO, chacun d'eux a également un UserDTO. Le problème est donc la grande quantité de données interrogées dans le backend et envoyées via le câble.
officier
Toutes les données sont-elles nécessaires?
Robert Harvey
1

Il est difficile d'établir les meilleures pratiques pour quelque chose d'aussi "flexible" ou abstrait qu'un DTO. Essentiellement, les DTO ne sont que des objets pour le transfert de données, mais selon la destination ou la raison du transfert, vous souhaiterez peut-être appliquer différentes "meilleures pratiques".

Je recommande de lire Patterns of Enterprise Application Architecture par Martin Fowler . Il y a tout un chapitre dédié aux modèles, où les DTO obtiennent une section vraiment détaillée.

À l'origine, ils étaient «conçus» pour être utilisés dans des appels à distance coûteux, où vous auriez probablement besoin de beaucoup de données provenant de différentes parties de votre logique; Les DTO feraient le transfert de données en un seul appel.

Selon l'auteur, les DTO n'étaient pas destinés à être utilisés dans des environnements locaux, mais certaines personnes en ont trouvé une utilisation. Habituellement, ils sont utilisés pour collecter des informations provenant de différents POCO dans une seule entité pour les interfaces graphiques, les API ou différentes couches.

Maintenant, avec l'héritage, la réutilisation du code ressemble plus à un effet secondaire de l'héritage qu'à son objectif principal; d'autre part, la composition est mise en œuvre avec la réutilisation du code comme objectif principal.

Certaines personnes recommandent d'utiliser la composition et l'héritage ensemble, en utilisant les forces des deux et en essayant d'atténuer leurs faiblesses. Ce qui suit fait partie de mon processus mental lors du choix ou de la création de nouveaux DTO, ou de toute nouvelle classe / objet d'ailleurs:

  • J'utilise l'héritage avec des DTO à l'intérieur de la même couche ou du même contexte. Un DTO n'héritera jamais d'un POCO, un BLL DTO n'héritera jamais d'un DAL DTO, etc.
  • Si je me retrouve à essayer de cacher un champ à un DTO, je vais refactoriser et peut-être utiliser la composition à la place.
  • Si très peu de champs différents d'un DTO de base sont tout ce dont j'ai besoin, je les mettrai dans un DTO universel. Les DTO universels ne sont utilisés qu'en interne.
  • Un POCO / DTO de base ne sera presque jamais utilisé pour une quelconque logique, de cette façon, la base ne répond qu'aux besoins de ses enfants. Si jamais j'ai besoin d'utiliser la base, j'évite d'ajouter tout nouveau champ que ses enfants n'utiliseront jamais.

Certaines d'entre elles ne sont peut-être pas les "meilleures" pratiques, elles fonctionnent assez bien pour les projets sur lesquels je travaille, mais vous devez vous rappeler qu'aucune taille ne convient à tous. Dans le cas du DTO universel, vous devez être prudent, mes signatures de méthodes ressemblent à ceci:

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

Si l'une des méthodes a besoin de son propre DTO, je fais l'héritage et généralement le seul changement que je dois faire est le paramètre, bien que parfois je doive creuser plus profondément pour des cas spécifiques.

D'après vos commentaires, je comprends que vous utilisez des DTO imbriqués. Si vos DTO imbriqués se composent uniquement d'une liste d'autres DTO, je pense que la meilleure chose à faire est de déballer la liste.

Selon la quantité de données que vous devez afficher ou utiliser, il peut être judicieux de créer de nouveaux DTO qui limitent les données; par exemple, si votre UserDTO a beaucoup de champs et que vous n'avez besoin que de 1 ou 2, il peut être préférable d'avoir un DTO avec uniquement ces champs. La définition de la couche, du contexte, de l'utilisation et de l'utilité d'un DTO aidera beaucoup lors de sa conception.

IvanGrasp
la source
Je n'ai pas pu ajouter plus de 2 liens dans ma réponse, voici quelques informations supplémentaires sur les DTO locaux. Je suis conscient que ses informations sont très anciennes, mais je pense que certaines d'entre elles sont peut-être encore pertinentes.
IvanGrasp
1

L'utilisation de la composition dans les DTO est une pratique parfaitement fine.

L'héritage parmi les types concrets de DTO est une mauvaise pratique.

D'une part, dans un langage comme C #, une propriété implémentée automatiquement a très peu de maintenance, donc les dupliquer (je déteste vraiment la duplication) n'est pas aussi nocif que souvent.

L' une des raisons pas utiliser l' héritage concret entre DTO est que certains outils leur carte heureusement pour les mauvais types.

Par exemple, si vous utilisez un utilitaire de base de données comme Dapper (je ne le recommande pas mais il est populaire) qui effectue l' class name -> table nameinférence, vous pourriez facilement finir par enregistrer un type dérivé en tant que type de base concret quelque part dans la hiérarchie et ainsi perdre des données ou pire .

Une raison plus profonde de ne pas utiliser l'héritage entre les DTO est qu'il ne doit pas être utilisé pour partager des implémentations entre des types qui n'ont pas de relation "est une" évidente. À mon avis, TaskDetailcela ne ressemble pas à un sous-type de Task. Cela pourrait tout aussi bien être la propriété d'un Taskou, pire, ce pourrait être un super-type de Task.

Maintenant, une chose qui pourrait vous préoccuper est de maintenir la cohérence entre les noms et les types des propriétés des divers DTO associés.

Alors que l'héritage de types concrets aiderait à garantir une telle cohérence, il est préférable d'utiliser des interfaces (ou des classes de base virtuelles pures en C ++) pour maintenir ce type de cohérence.

Considérez les DTO suivants

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
la source