Pourquoi hériter d'une classe et ne pas ajouter de propriétés?

39

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?

robotron
la source
25
Le revers n'est pas mentionné: vous pouvez distinguer les méthodes qui ne concernent que les OrderDateInfos de celles qui s'appliquent aux autres NamedEntitys
Caleth
8
Pour la même raison que si vous aviez par exemple une Identifierclasse n'ayant rien à voir avec celle NamedEntitynécessitant à la fois une propriété Idet une Namepropriété, vous ne l'utiliseriez pas NamedEntity. 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.
Neil

Réponses:

15

Pour moi, cela a du sens car cela donne un nom concret à la classe, au lieu de compter sur le nom 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?

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:

Onen'est pas une entité auditée ni une entité nommée. C'est simple:

public class One 
{ }

Twoest 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 
{ }

Threeest à la fois une entrée nommée et auditée. C'est là que vous rencontrez un problème. Vous pouvez créer une AuditedEntityclasse de base, mais vous ne pouvez pas faire Threehériter les 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 AuditedEntityhéritant de NamedEntity. C'est un astuce astucieuse 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 indique que chaque entité auditée est également une entité nommée . Ce qui m'amène à mon dernier exemple. Fourest une entité auditée mais pas une entité nommée. Mais vous ne pouvez pas laisser Fourhériter de AuditedEntitycomme vous le feriez également en NamedEntityraison de l'héritage entre AuditedEntity andNamedEntity`.

En utilisant l'héritage, il n'y a aucun moyen de faire les deux Threeet de Fourtravailler 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é:

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 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:

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'autre part, un certain nombre de ces classes n'ont tout simplement pas de propriétés supplémentaires.

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.

Flater
la source
4
"vous ne pouvez hériter que d'une classe, mais vous pouvez implémenter de nombreuses interfaces." Ce n'est pas vrai de toutes les langues, cependant. Certaines langues autorisent l'héritage de classes multiples.
Kenneth K.
Comme l'a dit Kenneth, deux langages très populaires ont la possibilité d'héritage multiple, C ++ et Python. class threedans 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.
quand
63

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 NamedEntitycomme 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 une SqlExceptionou une FileNotFoundExceptionexception personnalisée pour votre application. Souvent, ces classes ne fournissent pas plus de données ou de fonctionnalités que la Exceptionclasse 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?

Becuzz
la source
4
Pardonnez ma nouveauté, mais je suis curieux de savoir pourquoi ce ne serait pas une meilleure approche de faire de OrderDateInfo un objet NamedEntity en tant que propriété?
Adam B
9
@AdamB C'est un choix de conception valable. Que ce soit meilleur ou non dépend de ce pour quoi il sera utilisé, entre autres. Dans le cas des exceptions, je ne peux pas créer de nouvelle classe avec une propriété d'exception car le langage (C # pour moi) ne me permettra pas de le lancer. Il se peut que d'autres considérations de conception interdisent l'utilisation de la composition plutôt que de l'héritage. Plusieurs fois, la composition est meilleure. Cela dépend de votre système.
Becuzz
17
@AdamB Pour la même raison que lorsque vous héritez d'une classe de base et que vous ajoutez des méthodes ou des propriétés qui manquent à la base, vous ne créez pas de nouvelle classe et ne la mettez pas en tant que propriété. Il y a une différence entre être humain mammifère et avoir un mammifère.
Logarr
8
@Logarr true, il y a une différence entre être mammifère et avoir un mammifère. Mais si je reste fidèle à mon mammifère intérieur, vous ne saurez jamais la différence. Vous pourriez vous demander pourquoi je peux donner autant de types de lait sur un coup de tête.
candied_orange
5
@AdamB Vous pouvez le faire, et cela s'appelle une composition sur héritage. Mais vous renommeriez NamedEntitycela, par exemple EntityName, pour que la composition prenne tout son sens lorsqu'elle est décrite.
Sebastian Redl
28

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.

confits_orange
la source
1
Hmm, bon point concernant les exceptions. J'allais dire que c'était une chose horrible à faire. Mais vous m'avez convaincu du contraire avec un excellent cas d'utilisation.
David Arno
Yo-Yo Ho et une bouteille de rhum. Il est rare que l'orlop soit yar. Il y a souvent des fuites faute de joints appropriés entre les planches. Pour Davey Jones casier avec les lubrifiants terrestres ce qui le construit. TRADUCTION: Il est rare qu'une chaîne d'héritage soit si bonne que la classe de base puisse être ignorée de manière abstraite / efficace.
radarbob
Je pense qu'il est intéressant de souligner qu'une nouvelle classe héritant d'un autre n'ajouter une nouvelle propriété - le nom de la nouvelle classe. C'est exactement ce que vous utilisez lors de la création d'exceptions avec de meilleurs noms.
Logan Pickup
1

IMHO la conception de la classe est fausse. CA devrait etre.

public class EntityName
{
    public int Id { get; set; }
    public string Text { get; set; }
}

public class OrderDateInfo
{
    public EntityName Name { get; set; }
}

OrderDateInfoHAS A Nameest 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 NamedEntitytant que paramètre n'aurait été intéressé par le Idet Namepropriétés, de sorte que de telles méthodes doivent être modifiées pour accepter la EntityNameplace.

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!

batwad
la source
1

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:

Pourquoi avoir une structure de données de premier niveau commune?

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.

Pourquoi une structure de données qui inclut le plus haut niveau sans rien ajouter?

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.

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 Personstructure 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 que age, 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 agechamp plus tard). )

Edwin Buck
la source