Les objets de transfert de données ou POJO sont-ils censés être final
ou peuvent-ils être étendus et créer des hiérarchies pour eux?
Il n'est pas clair pour moi si une telle classe de valeur est correctement conçue uniquement comme une classe final
et quels sont les bons exemples/conceptions d'une hiérarchie de classes DTO?
Mise à jour: Réécriture du code en Java puisque cette question est balisée Java
mais cela s'applique à tout langage orienté objet.
Si l'utilisation de l'héritage est un moyen de réduire la duplication de code, je suis un peu réticent à ce que les DTO héritent de quoi que ce soit. L'héritage implique une relation est-une entre la classe enfant et le parent.
En utilisant des champs d'audit comme exemple, diriez-vous que l'énoncé suivant est vrai?
La personne est un BaseAuditFields.
Cela me semble assez drôle quand je le lis à haute voix. L'héritage dans les DTO devient problématique, car vous introduisez le couplage entre les classes où chaque classe représente généralement une idée concrète ou un cas d'utilisation. Les DTO partageant une classe parente commune impliquent que la hiérarchie des classes évoluera en même temps et pour les mêmes raisons. Cela rend ces classes plus difficiles à modifier sans avoir un effet d'entraînement en cascade vers les classes où les nouveaux champs peuvent ne pas être applicables.
Vous remarquerez que cela se produit plus souvent pour les DTO qui sérialisent et désérialisent les données à partir de sources sur lesquelles vous n'avez aucun contrôle, comme un service Web appartenant à une autre société ou équipe.
Cela étant dit, j'ai trouvé des cas où le traitement d'un DTO peut être abstrait. Les champs d'audit en sont un bon exemple. Dans ces cas, j'ai trouvé que les interfaces étaient une solution beaucoup plus robuste qu'une classe parent concrète, pour le simple fait qu'un DTO peut hériter de plusieurs interfaces. Même dans ce cas, je garde l'interface aussi concentrée que possible et je ne définis que les méthodes minimales pour récupérer ou modifier un petit sous-ensemble de champs:
public interface Auditable {
int getCreatorId();
void setCreatorId(int creatorId);
Integer getUpdaterId();
void setUpdaterId(Integer updaterId);
}
public class Person implements Auditable {
...
}
Maintenant, si nous revenons à notre test is-a:
La personne est vérifiable
La déclaration est plus logique. Avec l'utilisation d'interfaces, il devient plus facile d'utiliser le polymorphisme dans les classes qui traitent les DTO. Vous êtes maintenant libre d'ajouter et de supprimer des interfaces comme bon vous semble pour faciliter le traitement des DTO sans affecter la sérialisation et la désérialisation vers et depuis leur format source. Étant donné que des interfaces sont utilisées, vous pouvez modifier les classes pour refléter les changements structurels dans les données et disposer de méthodes supplémentaires qui rendent le DTO plus agréable au goût pour les classes qui les traitent.
Pour en revenir aux champs d'audit, un service Web appelle une propriété creatorId
et un autre l'appelle createUserId
. Si les deux DTO héritent de la même interface, le traitement des deux devient plus facile.
Supposons que vous disposiez d'un service Web pour le personnel des ventes. Par coïncidence, les noms de propriété dans la réponse XML correspondent à l'interface Auditable, donc rien de plus n'est nécessaire dans cette classe:
public class SalesStaffWebServicePerson implements Auditable {
private int creatorId;
private Date createDate;
private Integer updaterId;
private Date updatedDate;
public int getCreatorId() {
return creatorId;
}
public void setCreatorId(int creatorId) {
this.creatorId = creatorId;
}
public Date getCreateDate() {
return createDate;
}
public void setCreateDate(Date createDate) {
this.createDate = createDate;
}
public Integer getUpdaterId() {
return updaterId;
}
public void setUpdaterId(Integer updaterId) {
this.updaterId = updaterId;
}
public Date getUpdatedDate() {
return updatedDate;
}
public void setUpdatedDate(Date updatedDate) {
this.updatedDate = updatedDate;
}
}
Le service Web client renvoie une réponse JSON et deux champs sont nommés différemment de ce qui est défini dans l'interface Auditable, nous ajoutons donc quelques getters et setters à ce DTO afin de le rendre compatible:
public class CustomerWebServicePerson implements Auditable {
private int createUserId;
private Date createDate;
private Integer updateUserId;
private Date updatedDate;
// Getters/setters mapped over from customer web service JSON response
public int getCreateUserId() {
return createUserId;
}
public void setCreateUserId(int createUserId) {
this.createUserId = createUserId;
}
public Date getCreateDate() {
return createDate;
}
public void setCreateDate(Date createDate) {
this.createDate = createDate;
}
public Integer getUpdateUserId() {
return updateUserId;
}
public void setUpdateUserId(Integer updateUserId) {
this.updateUserId = updateUserId;
}
public Date getUpdatedDate() {
return updatedDate;
}
public void setUpdatedDate(Date updatedDate) {
this.updatedDate = updatedDate;
}
// Getters/setters to support Auditable interface
public int getCreatorId() {
return createUserId;
}
public void setCreatorId(int createUserId) {
this.createUserId = createUserId;
}
public Integer getUpdaterId() {
return updateUserId;
}
public void setUpdaterId(Integer updateUserId) {
this.updateUserId = updateUserId;
}
}
Bien sûr, il peut y avoir des getters et des setters en double, mais dans les coulisses, ils travaillent avec les mêmes domaines afin que tout le monde soit heureux.
Lorsque vous traitez avec des objets DTO, vous devez tenir compte des limites de votre moteur de désérialisation. Il y a des cas où avoir un objet de base est logique, mais vous ne pourrez peut-être pas l'utiliser sans sauter à travers des cerceaux. Prenons par exemple la hiérarchie suivante:
public class Observation {
public string SensorManufacturer { get; set; }
public string SensorEquipmentId { get; set; }
public float Reading { get; set; }
public Location Position { get; set; }
}
public class BizObservation : Observation {
public float BarSetting { get; set; }
}
Cette hiérarchie était vaguement basée sur un logiciel de surveillance que j'avais écrit pour un contrat il y a longtemps. La classe de base avait tout le nécessaire pour calculer la précision des observations, les taux de changement, la fréquence des erreurs, etc. basé sur le fabricant du capteur et l'ID de l'équipement.
L'un des défis était lié aux capacités de la bibliothèque que nous utilisions pour sérialiser et désérialiser les observations, car les observations devaient être transmises à un emplacement central.
Qu'est-ce qui peut mal tourner?
Aucun des problèmes n'est insurmontable, mais vous devrez probablement ajouter des informations à votre message pour choisir la bonne sous-classe. De nombreuses bibliothèques JSON fonctionnent très bien pour cela, mais si votre format de fil n'est pas JSON, votre kilométrage peut varier.
Conclusion
Les hiérarchies plates vont vous offrir l'expérience DTO la plus fluide et la moins complexe.
L'ajout de l'héritage ajoute de la complexité, mais certaines bibliothèques de sérialisation/désérialisation gèrent cette complexité dès le départ pour vous. D'autres exigent que vous sautiez à travers certains cercles pour que le système fonctionne de manière fiable.
Je pense qu'un cas pourrait être fait pour un pojo/poco/dto de base qui a des champs d'audit (c'est-à-dire WhenAdded, WhenDeleted, IsActive, etc.) Si ces champs sont communs à tous les schémas/tables, cela devrait aller. Tenez également compte de l'ORM (le cas échéant) que vous utilisez et de la façon dont il traite les pojos dérivés.
Exemple de hiérarchie:
class BaseAuditFields
{
Date LastUpdated;
Date WhenCreated;
String CreatedBy;
Bool IsDeleted;
}
class Person extends BaseAuditFields
{
String FirstName;
String LastName;
}
class Pet extends BaseAuditFields
{
String Species;
String Name;
String FavoriteToy;
}