J'ai trouvé un arbre d'héritage dans notre base de code (plutôt volumineuse) 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, c'est principalement utilisé pour relier des choses sur le front-end.
Pour moi, cela a du sens car cela donne un nom concret à la classe, au lieu de s’appuyer sur le générique NamedEntity
. D'autre part, un certain nombre de ces classes n'ont tout simplement pas de propriétés supplémentaires.
Y a-t-il des inconvénients à cette approche?
object-oriented-design
inheritance
robotron
la source
la source
OrderDateInfo
s de celles qui s'appliquent aux autresNamedEntity
sIdentifier
classe n'ayant rien à voir avec celleNamedEntity
nécessitant à la fois une propriétéId
et uneName
propriété, vous ne l'utiliseriez pasNamedEntity
. Le contexte et l'utilisation appropriée d'une classe ne se limitent pas aux propriétés et aux méthodes qu'elle contient.Réponses:
L’approche n’est pas mauvaise, mais de meilleures solutions sont disponibles. 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 que vous pouvez implémenter de nombreuses interfaces .
Par exemple, considérons 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:Two
est une entité nommée mais pas une entité auditée. C'est essentiellement ce que vous avez maintenant:Three
est à la fois une entrée nommée et auditée. C'est là que vous rencontrez un problème. Vous pouvez créer uneAuditedEntity
classe de base, mais vous ne pouvez pas faireThree
hériter les deuxAuditedEntity
etNamedEntity
:Cependant, vous pourriez penser à une solution de contournement en
AuditedEntity
héritant deNamedEntity
. C'est un astuce astucieuse pour s'assurer que chaque classe n'a besoin que d'hériter (directement) d'une autre classe.Cela fonctionne toujours. Mais ce que vous avez fait ici indique que chaque entité auditée est é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 laisserFour
hériter deAuditedEntity
comme vous le feriez également enNamedEntity
raison de l'héritage entre AuditedEntityand
NamedEntity`.En utilisant l'héritage, il n'y a aucun moyen de faire les deux
Three
et deFour
travailler si vous ne commencez pas à dupliquer les classes (ce qui crée un nouvel ensemble de problèmes).En utilisant des interfaces, ceci peut être facilement réalisé:
Le seul inconvénient mineur ici est que vous devez toujours implémenter l'interface. Mais vous bénéficiez de tous les avantages d'un type commun réutilisable, sans aucun des inconvénients qui apparaissent lorsque vous avez besoin de variations sur plusieurs types communs pour une entité donnée.
Mais votre polymorphisme reste intact:
Il s'agit d'une variante du modèle d'interface de marqueur , dans lequel vous implémentez une interface vide uniquement pour pouvoir utiliser le type d'interface afin de 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 l'objectif est le même. Je vais donc parler d'une "classe marquée".
À première vue, il n'y a rien de mal avec les interfaces / classes de marqueurs. Ils sont valides du point de vue syntaxique et technique, et leur utilisation ne présente aucun inconvénient, à condition que le marqueur soit universellement vrai (au moment de la compilation) et non conditionnel .
C’est exactement la manière dont vous devriez différencier les différentes exceptions, même si ces exceptions n’ont pas de propriétés / méthodes supplémentaires par rapport à la méthode de base.
Il n’ya donc rien de mal à cela, mais je vous conseillerais de ne pas dissimuler une erreur architecturale existante avec un polymorphisme mal conçu.
la source
three
dans votre exemple des pièges de la non-utilisation des interfaces est valide en C ++ par exemple, en fait cela fonctionne correctement pour les quatre exemples. En fait, tout cela semble être une limitation de C # en tant que langage plutôt qu'un récit édifiant de ne pas utiliser l'héritage lorsque vous devriez utiliser des interfaces.Ceci est quelque chose que j'utilise pour empêcher le polymorphisme d'être utilisé.
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" juste écrire la signature comme
void MyMethodThatShouldOnlyTakeOrderDateInfos(NamedEntity foo)
Et espère et prie que personne n’abuse du système de typage 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 comptez pas sur les personnes qui ne font pas d'erreur.En outre, comme @candied_orange l’a souligné , les exceptions en sont un excellent exemple. Très rarement (et je veux dire très, très, très rarement) avez-vous jamais envie de tout attraper avec
catch (Exception e)
. Il est plus probable que vous souhaitiez intercepter uneSqlException
ou uneFileNotFoundException
exception personnalisée pour votre application. Souvent, ces classes ne fournissent pas plus de données ou de fonctionnalités que laException
classe de base , 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.Globalement, c'est une astuce pour que le système de types vous permette d'utiliser un ensemble de types plus restreint que celui que vous pourriez utiliser si vous utilisiez une classe de base. Je veux dire, vous pourriez définir toutes vos variables et tous vos arguments comme ayant le type
Object
, mais cela ne ferait que rendre votre travail plus difficile, n'est-ce pas?la source
NamedEntity
cela, par exempleEntityName
, pour que la composition prenne tout son sens lorsqu'elle est décrite.C'est mon utilisation préférée de l'héritage. Je l'utilise principalement pour les exceptions qui pourraient utiliser des noms meilleurs, plus spécifiques
La question habituelle de l'héritage menant à de longues chaînes et causant le problème du yo-yo ne s'applique pas ici car il n'y a rien qui vous motive à chaîner.
la source
IMHO la conception de la classe est fausse. CA devrait etre.
OrderDateInfo
HAS AName
est une relation plus naturelle et crée deux classes faciles à comprendre qui n'auraient pas provoqué la question initiale.Toute méthode qui a accepté en
NamedEntity
tant que paramètre n'aurait été intéressé par leId
etName
propriétés, de sorte que de telles méthodes doivent être modifiées pour accepter laEntityName
place.La seule raison technique que j'accepterais pour la conception d'origine est la liaison de propriété, mentionnée par l'OP. Un framework de merde ne serait pas capable de gérer la propriété supplémentaire et de s'y lier
object.Name.Id
. Mais si votre cadre contraignant ne peut pas faire face à cela, alors 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!
la source
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 ordre d'idées, je vais reformuler votre question comme suit:
Vous pouvez donc utiliser le type de données de niveau supérieur. Cela permet de tirer parti du système de types pour garantir une stratégie sur de grands ensembles de structures de données différentes en garantissant que toutes les propriétés sont présentes.
Vous pouvez donc utiliser le type de données de niveau inférieur. Cela permet de mettre des indices dans le système de dactylographie pour exprimer le but de la variable.
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
Person
structure de données, 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 telles queage
, etc.Il s’agit donc de tirer parti du système de dactylographie 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 qui peut être étendue facilement à une date ultérieure (si vous avez réellement besoin du
age
champ plus tard). )la source