Nous avons une API Web ASP.NET qui fournit une API REST pour notre application à page unique. Nous utilisons des DTO/POCO pour transmettre des données via cette API.
Le problème est maintenant que ces DTO deviennent plus gros au fil du 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, il existe des DTO étendus qui 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 de terminaison/méthodes (par exemple, GET et PUT), ils ont été étendus progressivement par 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?
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, peut-être juste le nom de l'utilisateur auquel la tâche est affecté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, alors modélisez 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é.
À 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 disposez d'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 à vos points de terminaison pour rassembler les informations nécessaires: nom, adresse de facturation, adresse de livraison, éléments de ligne. Dans un contexte de transaction commerciale, un DTO unique à partir d'un seul point de terminaison peut renvoyer toutes ces informations à la fois.
L'utilisation de la composition dans les DTO est une pratique parfaitement fine.
L'héritage entre 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.
Une raison pas d'utiliser l'héritage concret parmi les DTO est que certains outils les associeront volontiers aux 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 exécute class name -> table name
infé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 pour 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, TaskDetail
ne ressemble pas à un sous-type de Task
. Cela pourrait tout aussi bien être une propriété d'un Task
ou, 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 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
}
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 la lecture 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:
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.