J'ai trouvé un arbre d'héritage dans notre base de code (plutôt grande) qui ressemble à ceci:
public class NamedEntity
{
public int Id { get; set; }
public string Name { get; set; }
}
public class OrderDateInfo : NamedEntity { }
D'après ce que j'ai pu comprendre, cela est principalement utilisé pour lier des choses sur le front-end.
Pour moi, cela a du sens car il donne un nom concret à la classe, au lieu de s'appuyer sur le générique NamedEntity
. D'un autre côté, il existe un certain nombre de ces classes qui n'ont tout simplement pas de propriétés supplémentaires.
Y a-t-il des inconvénients à cette approche?
Pour moi, cela a du sens car il donne un nom concret à la classe, au lieu de s'appuyer sur le NamedEntity générique. D'un autre côté, il existe un certain nombre de ces classes qui n'ont tout simplement pas de propriétés supplémentaires.
Y a-t-il des inconvénients à cette approche?
L'approche n'est pas mauvaise, mais il existe de meilleures solutions. En bref, une interface serait une bien meilleure solution pour cela. La principale raison pour laquelle les interfaces et l'héritage sont différents ici est que vous ne pouvez hériter que d'une classe, mais vous pouvez implémenter de nombreuses interfaces .
Par exemple, considérez que vous avez nommé des entités et des entités auditées. Vous avez plusieurs entités:
One
n'est pas une entité auditée ni une entité nommée. C'est simple:
public class One
{ }
Two
est une entité nommée mais pas une entité auditée. C'est essentiellement ce que vous avez maintenant:
public class NamedEntity
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Two : NamedEntity
{ }
Three
est à la fois une entrée nommée et vérifiée. C'est là que vous rencontrez un problème. Vous pouvez créer une classe de base AuditedEntity
, mais vous ne pouvez pas faire en sorte que Three
hérite des deux AuditedEntity
et NamedEntity
:
public class AuditedEntity
{
public DateTime CreatedOn { get; set; }
public DateTime UpdatedOn { get; set; }
}
public class Three : NamedEntity, AuditedEntity // <-- Compiler error!
{ }
Cependant, vous pourriez penser à une solution de contournement en ayant AuditedEntity
hériter de NamedEntity
. C'est un hack intelligent pour s'assurer que chaque classe n'a besoin que d'hériter (directement) d'une autre classe.
public class AuditedEntity : NamedEntity
{
public DateTime CreatedOn { get; set; }
public DateTime UpdatedOn { get; set; }
}
public class Three : AuditedEntity
{ }
Cela fonctionne toujours. Mais ce que vous avez fait ici est indiqué que chaque entité auditée est intrinsèquement également une entité nommée. Ce qui m'amène à mon dernier exemple. Four
est une entité auditée mais pas une entité nommée. Mais vous ne pouvez pas laisser Four
hériter de AuditedEntity
comme vous le feriez alors également en NamedEntity
en raison de l'héritage entre AuditedEntity and
NamedEntity`.
En utilisant l'héritage, il n'y a aucun moyen de faire fonctionner Three
et Four
à moins que vous ne commenciez à dupliquer des classes (ce qui ouvre un tout nouvel ensemble de problèmes).
En utilisant des interfaces, cela peut facilement être réalisé:
public interface INamedEntity
{
int Id { get; set; }
string Name { get; set; }
}
public interface IAuditedEntity
{
DateTime CreatedOn { get; set; }
DateTime UpdatedOn { get; set; }
}
public class One
{ }
public class Two : INamedEntity
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Three : INamedEntity, IAuditedEntity
{
public int Id { get; set; }
public string Name { get; set; }
DateTime CreatedOn { get; set; }
DateTime UpdatedOn { get; set; }
}
public class Four : IAuditedEntity
{
DateTime CreatedOn { get; set; }
DateTime UpdatedOn { get; set; }
}
Le seul inconvénient mineur ici est que vous devez toujours implémenter l'interface. Mais vous obtenez tous les avantages d'avoir un type réutilisable commun, sans aucun des inconvénients qui émergent lorsque vous avez besoin de variations sur plusieurs types communs pour une entité donnée.
Mais votre polymorphisme reste intact:
var one = new One();
var two = new Two();
var three = new Three();
var four = new Four();
public void HandleNamedEntity(INamedEntity namedEntity) {}
public void HandleAuditedEntity(IAuditedEntity auditedEntity) {}
HandleNamedEntity(one); //Error - not a named entity
HandleNamedEntity(two);
HandleNamedEntity(three);
HandleNamedEntity(four); //Error - not a named entity
HandleAuditedEntity(one); //Error - not an audited entity
HandleAuditedEntity(two); //Error - not an audited entity
HandleAuditedEntity(three);
HandleAuditedEntity(four);
D'un autre côté, il existe un certain nombre de ces classes qui n'ont tout simplement pas de propriétés supplémentaires.
Il s'agit d'une variante du modèle d'interface marqueur , où vous implémentez une interface vide uniquement pour pouvoir utiliser le type d'interface pour vérifier si une classe donnée est "marquée" avec cette interface.
Vous utilisez des classes héritées au lieu d'interfaces implémentées, mais le but est le même, donc je vais y faire référence comme une "classe marquée".
À première vue, il n'y a rien de mal avec les interfaces/classes de marqueurs. Ils sont valides syntaxiquement et techniquement, et il n'y a aucun inconvénient inhérent à les utiliser à condition que le marqueur soit universellement vrai (au moment de la compilation) et non conditionnel .
C'est exactement comme cela que vous devez différencier les différentes exceptions, même lorsque ces exceptions n'ont pas réellement de propriétés/méthodes supplémentaires par rapport à la méthode de base.
Il n'y a donc rien de mal à le faire, mais je vous conseille d'utiliser cela avec prudence, en vous assurant que vous n'essayez pas simplement de couvrir une erreur architecturale existante avec un polymorphisme mal conçu.
C'est quelque chose que j'utilise pour empêcher l'utilisation du polymorphisme.
Supposons que vous avez 15 classes différentes qui ont NamedEntity
comme classe de base quelque part dans leur chaîne d'héritage et que vous écrivez une nouvelle méthode qui ne s'applique qu'à OrderDateInfo
.
Vous "pourriez" simplement écrire la signature
void MyMethodThatShouldOnlyTakeOrderDateInfos(NamedEntity foo)
Et espérons et priez que personne n'abuse du système de caractères pour y insérer un FooBazNamedEntity
.
Ou vous "pourriez" simplement écrire void MyMethod(OrderDateInfo foo)
. Maintenant, cela est imposé par le compilateur. Simple, élégant et ne dépend pas des gens qui ne font pas d'erreurs.
En outre, comme l'a souligné @candied_orange , les exceptions en sont un excellent exemple. Très rarement (et je veux dire très, très, très rarement) voulez-vous tout attraper avec catch (Exception e)
. Il est plus probable que vous souhaitiez intercepter un SqlException
ou un FileNotFoundException
ou une exception personnalisée pour votre application. Ces classes ne fournissent souvent pas plus de données ou de fonctionnalités que la classe de base Exception
, mais elles vous permettent de différencier ce qu'elles représentent sans avoir à les inspecter et à vérifier un champ de type ou à rechercher un texte spécifique.
Dans l'ensemble, c'est une astuce pour obtenir le système de types pour vous permettre d'utiliser un ensemble de types plus étroit que vous ne le pourriez si vous utilisiez une classe de base. Je veux dire, vous pourriez définir toutes vos variables et arguments comme ayant le type Object
, mais cela rendrait votre travail plus difficile, non?
Ceci est mon utilisation préférée de l'héritage. Je l'utilise principalement pour des exceptions qui pourraient utiliser des noms meilleurs et plus spécifiques
Le problème habituel de l'héritage conduisant à de longues chaînes et provoquant le problème yo-yo ne s'applique pas ici car il n'y a rien pour vous motiver à enchaîner.
À mon humble avis, la conception de la classe est fausse. Ça devrait être.
public class EntityName
{
public int Id { get; set; }
public string Text { get; set; }
}
public class OrderDateInfo
{
public EntityName Name { get; set; }
}
OrderDateInfo
A UNE Name
est une relation plus naturelle et crée deux classes faciles à comprendre qui n'auraient pas provoqué la question d'origine.
Toute méthode qui a accepté NamedEntity
en tant que paramètre n'aurait dû être intéressée que par les propriétés Id
et Name
, donc toutes ces méthodes devraient être modifiées pour accepter EntityName
au lieu.
La seule raison technique que j'accepterais pour la conception originale est la reliure de propriété, mentionnée par le PO. Un cadre de merde ne serait pas en mesure de faire face à la propriété supplémentaire et de se lier à object.Name.Id
. Mais si votre cadre contraignant ne peut pas faire face à cela, vous avez encore une dette technologique à ajouter à la liste.
Je serais d'accord avec la réponse de @ Flater, mais avec beaucoup d'interfaces contenant des propriétés, vous finissez par écrire beaucoup de code standard, même avec les belles propriétés automatiques de C #. Imaginez le faire en Java!
Les classes exposent le comportement et les structures de données exposent les données.
Je vois les mots clés de la classe, mais je ne vois aucun comportement. Cela signifie que je commencerais à voir cette classe comme une structure de données. Dans cet esprit, je vais reformuler votre question comme
Pourquoi avoir une structure de données commune de premier niveau?
Vous pouvez donc utiliser le type de données de niveau supérieur. Cela permet de tirer parti du système de type pour garantir une stratégie sur de grands ensembles de différentes structures de données en s'assurant que les propriétés sont toutes là.
Pourquoi avoir une structure de données qui inclut celle de niveau supérieur, mais n'ajoute rien?
Vous pouvez donc utiliser le type de données de niveau inférieur. Cela permet de mettre des indices dans le système de frappe pour exprimer le but de la variable
Top level data structure - Named
property: name;
Bottom level data structure - Person
Dans la hiérarchie ci-dessus, nous trouvons pratique de spécifier qu'une personne est nommée, afin que les personnes puissent obtenir et modifier leur nom. Bien qu'il puisse être raisonnable d'ajouter des propriétés supplémentaires à la structure de données Person
, le problème que nous résolvons ne nécessite pas une modélisation parfaite de la personne, et nous avons donc négligé d'ajouter des propriétés communes comme age
, etc.
C'est donc un effet de levier du système de saisie pour exprimer l'intention de cet élément nommé d'une manière qui ne rompt pas avec les mises à jour (comme la documentation) et peut être étendu à une date ultérieure avec facilité (si vous trouvez que vous avez vraiment besoin du age
champ plus tard).