Pourquoi ne pas hériter de List <T>?

1400

Lors de la planification de mes programmes, je commence souvent par une chaîne de pensée comme celle-ci:

Une équipe de football n'est qu'une liste de joueurs de football. Par conséquent, je devrais le représenter avec:

var football_team = new List<FootballPlayer>();

L'ordre de cette liste représente l'ordre dans lequel les joueurs sont répertoriés dans la liste.

Mais je me rends compte plus tard que les équipes ont également d'autres propriétés, en plus de la simple liste de joueurs, qui doivent être enregistrées. Par exemple, le total cumulé des scores de cette saison, le budget actuel, les couleurs uniformes, un stringreprésentant le nom de l'équipe, etc.

Alors je pense:

D'accord, une équipe de football est comme une liste de joueurs, mais en plus, elle a un nom (a string) et un total cumulé de scores (an int). .NET ne fournit pas de classe pour le stockage des équipes de football, je vais donc créer ma propre classe. La structure existante la plus similaire et la plus pertinente est List<FootballPlayer>, donc j'en hériterai:

class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal 
}

Mais il s'avère qu'une directive dit que vous ne devriez pas hériter deList<T> . Je suis profondément confus par cette directive à deux égards.

Pourquoi pas?

Apparemment, il Listest en quelque sorte optimisé pour les performances . Comment? Quels problèmes de performances vais-je causer si je prolonge List? Qu'est-ce qui se cassera exactement?

Une autre raison que j'ai vue est celle Listfournie par Microsoft, et je n'ai aucun contrôle dessus, donc je ne peux pas la changer plus tard, après avoir exposé une "API publique" . Mais j'ai du mal à comprendre cela. Qu'est-ce qu'une API publique et pourquoi devrais-je m'en soucier? Si mon projet actuel ne possède pas et n'est pas susceptible d'avoir cette API publique, puis-je ignorer cette directive en toute sécurité? Si j'hérite List et qu'il s'avère que j'ai besoin d'une API publique, quelles difficultés aurai-je?

Pourquoi est-ce même important? Une liste est une liste. Qu'est-ce qui pourrait éventuellement changer? Que pourrais-je vouloir changer?

Et enfin, si Microsoft ne voulait pas que j'hérite List, pourquoi n'ont-ils pas fait la classe sealed?

Que dois-je utiliser d'autre?

Apparemment, pour les collections personnalisées, Microsoft a fourni une Collectionclasse qui devrait être étendue au lieu de List. Mais cette classe est très nue et n'a pas beaucoup de choses utiles, commeAddRange par exemple. La réponse de jvitor83 fournit une justification des performances pour cette méthode particulière, mais comment un lent AddRangen'est-il pas meilleur que non AddRange?

Hériter de Collectionest bien plus de travail que d'hériter List, et je ne vois aucun avantage. Certes, Microsoft ne me dirait pas de faire du travail supplémentaire sans raison, donc je ne peux m'empêcher de me sentir comme si je comprenais quelque chose, et hériter Collectionn'est en fait pas la bonne solution pour mon problème.

J'ai vu des suggestions telles que la mise en œuvre IList. Tout simplement pas. Ce sont des dizaines de lignes de code passe-partout qui ne me rapportent rien.

Enfin, certains suggèrent d'envelopper Listquelque chose:

class FootballTeam 
{ 
    public List<FootballPlayer> Players; 
}

Il y a deux problèmes avec ceci:

  1. Cela rend mon code inutilement verbeux. Je dois maintenant appeler my_team.Players.Countau lieu de juste my_team.Count. Heureusement, avec C # je peux définir des indexeurs pour rendre l'indexation transparente, et transmettre toutes les méthodes de l'interne List... Mais c'est beaucoup de code! Qu'est-ce que j'obtiens pour tout ce travail?

  2. Cela n'a tout simplement aucun sens. Une équipe de football n'a pas de "liste" de joueurs. Il est la liste des joueurs. Vous ne dites pas "John McFootballer a rejoint les joueurs de SomeTeam". Vous dites "John a rejoint SomeTeam". Vous n'ajoutez pas de lettre aux "caractères d'une chaîne", vous ajoutez une lettre à une chaîne. Vous n'ajoutez pas un livre aux livres d'une bibliothèque, vous ajoutez un livre à une bibliothèque.

Je me rends compte que ce qui se passe "sous le capot" peut être considéré comme "l'ajout de X à la liste interne de Y", mais cela semble être une façon très contre-intuitive de penser le monde.

Ma question (résumée)

Quelle est la bonne façon de C # de représenter une structure de données, qui, « logiquement » (c'est - à - dire, « à l'esprit humain ») est juste listde thingsquelques cloches et de sifflets?

L'héritage est-il List<T>toujours inacceptable? Quand est-ce acceptable? Pourquoi pourquoi pas? De quoi un programmeur doit-il tenir compte lorsqu'il décide d'hériter List<T>ou non?

Superbest
la source
90
Donc, votre question est-elle vraiment de savoir quand utiliser l'héritage (un carré est un rectangle car il est toujours raisonnable de fournir un carré lorsqu'un rectangle générique a été demandé) et quand utiliser la composition (une FootballTeam a une liste de FootballPlayers, en plus à d'autres propriétés qui lui sont tout aussi "fondamentales", comme un nom)? Les autres programmeurs seraient-ils confus si, ailleurs dans le code, vous leur transmettiez une FootballTeam alors qu'ils attendaient une simple liste?
Flight Odyssey
77
que se passe-t-il si je vous dis qu'une équipe de football est une entreprise commerciale qui POSSÈDE une liste de contrats de joueurs au lieu d'une simple liste de joueurs avec des propriétés supplémentaires?
STT LCU
75
C'est vraiment assez simple. Une équipe de football est-elle une liste de joueurs? Évidemment non, car comme vous le dites, il existe d'autres propriétés pertinentes. Une équipe de football a-t-elle une liste de joueurs? Oui, il devrait donc contenir une liste. En dehors de cela, la composition est juste préférable à l'héritage en général, car elle est plus facile à modifier plus tard. Si vous trouvez l'héritage et la composition logiques en même temps, optez pour la composition.
Tobberoth
45
@FlightOdyssey: Supposons que vous ayez une méthode SquashRectangle qui prend un rectangle, divise par deux sa hauteur et double sa largeur. Passez maintenant un rectangle qui se trouve être 4x4 mais qui est de type rectangle, et un carré qui est 4x4. Quelles sont les dimensions de l'objet après l'appel de SquashRectangle? Pour le rectangle, c'est clairement 2x8. Si le carré est 2x8 alors ce n'est plus un carré; si ce n'est pas 2x8, la même opération sur la même forme produit un résultat différent. La conclusion est qu'un carré mutable n'est pas une sorte de rectangle.
Eric Lippert
29
@Gangnus: Votre déclaration est tout simplement fausse; une vraie sous-classe est nécessaire pour avoir une fonctionnalité qui est un sur - ensemble de la superclasse. Un stringest requis pour faire tout ce qu'un objectpeut faire et plus encore .
Eric Lippert

Réponses:

1567

Il y a de bonnes réponses ici. Je voudrais leur ajouter les points suivants.

Quelle est la bonne façon C # de représenter une structure de données qui, "logiquement" (c'est-à-dire "pour l'esprit humain") n'est qu'une liste de choses avec quelques cloches et sifflets?

Demandez à dix personnes qui ne sont pas programmeurs informatiques et qui connaissent bien le football de remplir le blanc:

Une équipe de football est un type particulier de _____

Quelqu'un a- t -il dit "liste des joueurs de football avec quelques cloches et sifflets", ou ont-ils tous dit "équipe sportive" ou "club" ou "organisation"? Votre idée qu'une équipe de football est un type particulier de liste de joueurs est dans votre esprit humain et votre esprit humain seul.

List<T>est un mécanisme . L'équipe de football est un objet commercial - c'est-à-dire un objet qui représente un concept qui est dans le domaine commercial du programme. Ne les mélangez pas! Une équipe de football est une sorte d' équipe; il a une liste, une liste est une liste de joueurs . Une liste n'est pas un type particulier de liste de joueurs . Une liste est une liste de joueurs. Donc, faites une propriété appelée Rosterc'est un List<Player>. Et faites-le ReadOnlyList<Player>pendant que vous y êtes, sauf si vous pensez que tous ceux qui connaissent une équipe de football peuvent supprimer des joueurs de la liste.

L'héritage est-il List<T>toujours inacceptable?

Inacceptable pour qui? Moi? Non.

Quand est-ce acceptable?

Lorsque vous créez un mécanisme qui étend le List<T>mécanisme .

Que doit prendre en compte un programmeur lorsqu'il décide d'hériter de List<T> ou non?

Suis-je en train de construire un mécanisme ou un objet métier ?

Mais ça fait beaucoup de code! Qu'est-ce que j'obtiens pour tout ce travail?

Vous avez passé plus de temps à taper votre question qu'il vous aurait fallu pour écrire des méthodes de transfert pour les membres concernés de List<T> cinquante fois . Vous n'avez clairement pas peur de la verbosité, et nous parlons ici d'une très petite quantité de code; c'est un travail de quelques minutes.

MISE À JOUR

J'ai réfléchi davantage et il y a une autre raison de ne pas modéliser une équipe de football comme une liste de joueurs. En fait, ce pourrait être une mauvaise idée de modéliser une équipe de football comme ayant également une liste de joueurs. Le problème avec une équipe en tant que / ayant une liste de joueurs est que ce que vous avez est un instantané de l'équipe à un moment donné . Je ne sais pas quelle est votre analyse de rentabilisation pour cette classe, mais si j'avais une classe qui représentait une équipe de football, je voudrais lui poser des questions comme "combien de joueurs des Seahawks ont raté des matchs en raison d'une blessure entre 2003 et 2013?" ou "Quel joueur de Denver qui jouait auparavant pour une autre équipe a connu la plus forte augmentation d'une année à l'autre de verges?" ou " Les Piggers sont-ils allés jusqu'au bout cette année? "

C'est-à-dire qu'une équipe de football me semble bien modelée comme un ensemble de faits historiques tels que le moment où un joueur a été recruté, blessé, retraité, etc. Évidemment, la liste actuelle des joueurs est un fait important qui devrait probablement être à l'avant-plan. centre, mais il peut y avoir d'autres choses intéressantes que vous voulez faire avec cet objet qui nécessitent une perspective plus historique.

Eric Lippert
la source
84
Bien que les autres réponses aient été très utiles, je pense que celle-ci répond le plus directement à mes préoccupations. En ce qui concerne l'exagération de la quantité de code, vous avez raison de dire que ce n'est pas beaucoup de travail à la fin, mais je m'embrouille très facilement lorsque j'inclus du code dans mon programme sans comprendre pourquoi je l'inclus.
Superbest
128
@Superbest: content d'avoir pu aider! Vous avez raison d'écouter ces doutes; si vous ne comprenez pas pourquoi vous écrivez du code, essayez de le comprendre ou écrivez un code différent que vous comprenez.
Eric Lippert
48
@Mehrdad Mais vous semblez oublier les règles d'or de l'optimisation: 1) ne le faites pas, 2) ne le faites pas encore, et 3) ne le faites pas sans d'abord faire des profils de performances pour montrer ce qui doit être optimisé.
AJMansfield
107
@Mehrdad: Honnêtement, si votre application nécessite que vous vous souciez du fardeau des performances des méthodes virtuelles, par exemple, tout langage moderne (C #, Java, etc.) n'est pas le langage pour vous. J'ai un ordinateur portable ici qui pourrait exécuter une simulation de tous les jeux d'arcade auxquels j'ai joué en tant qu'enfant simultanément . Cela me permet de ne pas m'inquiéter d'un niveau d'indirection ici et là.
Eric Lippert
44
@Superbest: Maintenant, nous sommes définitivement à l'extrémité "composition et non héritage" du spectre. Une fusée est composée de parties de fusée . Une fusée n'est pas "un type spécial de liste de pièces"! Je vous suggère de le réduire davantage; pourquoi une liste, par opposition à une IEnumerable<T>- une séquence ?
Eric Lippert
161

Wow, votre message contient toute une série de questions et de points. La plupart des raisonnements que vous obtenez de Microsoft sont exactement sur le point. Commençons par tout ce qui concerneList<T>

  • List<T> est hautement optimisé. Son utilisation principale doit être utilisée en tant que membre privé d'un objet.
  • Microsoft n'a pas sceller parce que parfois vous pouvez créer une classe qui a un nom de plus convivial: class MyList<T, TX> : List<CustomObject<T, Something<TX>> { ... }. C'est aussi simple que de le fairevar list = new MyList<int, string>(); .
  • CA1002: N'exposez pas les listes génériques : Fondamentalement, même si vous prévoyez d'utiliser cette application en tant que développeur unique, il vaut la peine de développer avec de bonnes pratiques de codage, de sorte qu'elles deviennent instillées en vous et une seconde nature. Vous êtes toujours autorisé à exposer la liste comme IList<T>si vous avez besoin d'un consommateur pour avoir une liste indexée. Cela vous permet de modifier l'implémentation au sein d'une classe ultérieurement.
  • Microsoft l'a rendu Collection<T>très générique parce que c'est un concept générique ... le nom dit tout; ce n'est qu'une collection. Il existe des versions plus précises telles que SortedCollection<T>, ObservableCollection<T>, ReadOnlyCollection<T>, etc. , qui mettent en œuvre chacun , IList<T>mais pas List<T> .
  • Collection<T>permet aux membres (c'est-à-dire ajouter, supprimer, etc.) d'être remplacés car ils sont virtuels. List<T>ne fait pas.
  • La dernière partie de votre question est juste. Une équipe de football est plus qu'une simple liste de joueurs, donc ce devrait être une classe qui contient cette liste de joueurs. Pensez composition vs héritage . Une équipe de football a une liste de joueurs (une liste), ce n'est pas une liste de joueurs.

Si j'écrivais ce code, la classe ressemblerait probablement à ceci:

public class FootballTeam
{
    // Football team rosters are generally 53 total players.
    private readonly List<T> _roster = new List<T>(53);

    public IList<T> Roster
    {
        get { return _roster; }
    }

    // Yes. I used LINQ here. This is so I don't have to worry about
    // _roster.Length vs _roster.Count vs anything else.
    public int PlayerCount
    {
        get { return _roster.Count(); }
    }

    // Any additional members you want to expose/wrap.
}
mon
la source
6
" votre message contient un tas de questions " - Eh bien, j'ai dit que j'étais complètement confus. Merci d'avoir créé un lien vers la question de @ Readonly, je vois maintenant qu'une grande partie de ma question est un cas particulier du problème plus général de composition vs héritage.
Superbe
3
@Brian: Sauf que vous ne pouvez pas créer de générique à l'aide d'un alias, tous les types doivent être concrets. Dans mon exemple, List<T>est étendu comme une classe interne privée qui utilise des génériques pour simplifier l'utilisation et la déclaration de List<T>. Si l'extension était simplement class FootballRoster : List<FootballPlayer> { }, alors oui, j'aurais pu utiliser un alias à la place. Je suppose que cela vaut la peine de noter que vous pouvez ajouter des membres / fonctionnalités supplémentaires, mais c'est limité car vous ne pouvez pas remplacer les membres importants de List<T>.
mon
6
S'il s'agissait de Programmers.SE, je serais d'accord avec la gamme actuelle de votes de réponse, mais, comme il s'agit d'un article SO, cette réponse tente au moins de répondre aux problèmes spécifiques C # (.NET), même s'il existe des les problèmes de conception.
Mark Hurd
7
Collection<T> allows for members (i.e. Add, Remove, etc.) to be overriddenMaintenant , c'est une bonne raison de ne pas hériter de la liste. Les autres réponses ont beaucoup trop de philosophie.
Navin
10
@my: Je suppose que vous voulez dire "une flopée" de questions, car un détective est un détective.
batty
152
class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal;
}

Le code précédent signifie: un groupe de gars de la rue jouant au football, et ils ont un nom. Quelque chose comme:

Les gens jouent au football

Quoi qu'il en soit, ce code (d'après ma réponse)

public class FootballTeam
{
    // A team's name
    public string TeamName; 

    // Football team rosters are generally 53 total players.
    private readonly List<T> _roster = new List<T>(53);

    public IList<T> Roster
    {
        get { return _roster; }
    }

    public int PlayerCount
    {
        get { return _roster.Count(); }
    }

    // Any additional members you want to expose/wrap.
}

Moyens: c'est une équipe de football qui a la gestion, les joueurs, les administrateurs, etc. Quelque chose comme:

Image montrant les membres (et autres attributs) de l'équipe de Manchester United

Voilà comment votre logique est présentée en images…

Nean Der Thal
la source
9
Je suppose que votre deuxième exemple est un club de football, pas une équipe de football. Un club de football a managers et admin , etc. De cette source : « Une équipe de football est le nom collectif donné à un groupe de joueurs ... Ces équipes pourraient être sélectionnés pour jouer dans un match contre une équipe adverse, pour représenter un club de football ... ". Je pense donc qu'une équipe est une liste de joueurs (ou peut-être plus précisément une collection de joueurs).
Ben
3
Plus optimiséprivate readonly List<T> _roster = new List<T>(44);
SiD
2
Il est nécessaire d'importer un System.Linqespace de noms.
Davide Cannizzo
1
Pour les visuels: +1
V.7
115

Ceci est un exemple classique de composition vs héritage .

Dans ce cas précis:

L'équipe est-elle une liste de joueurs avec un comportement supplémentaire

ou

L'équipe est-elle un objet à part entière qui contient une liste de joueurs?

En étendant List, vous vous limitez de plusieurs façons:

  1. Vous ne pouvez pas restreindre l'accès (par exemple, empêcher des personnes de modifier la liste). Vous obtenez toutes les méthodes List, que vous en ayez besoin ou que vous les vouliez toutes ou non.

  2. Que se passe-t-il si vous souhaitez également avoir des listes d'autres choses. Par exemple, les équipes ont des entraîneurs, des managers, des fans, de l'équipement, etc. Certains d'entre eux pourraient bien être des listes à part entière.

  3. Vous limitez vos options d'héritage. Par exemple, vous souhaiterez peut-être créer un objet Team générique, puis avoir BaseballTeam, FootballTeam, etc. qui en héritera. Pour hériter de List, vous devez faire l'héritage de Team, mais cela signifie que tous les différents types d'équipe sont obligés d'avoir la même implémentation de cette liste.

Composition - y compris un objet donnant le comportement que vous souhaitez à l'intérieur de votre objet.

Héritage - votre objet devient une instance de l'objet qui a le comportement que vous souhaitez.

Les deux ont leurs utilisations, mais c'est un cas clair où la composition est préférable.

Tim B
la source
1
S'étendant sur # 2, il n'est pas logique qu'une liste ait une liste.
Cruncher
Premièrement, la question n'a rien à voir avec la composition vs l'héritage. La réponse est que OP ne veut pas implémenter un type de liste plus spécifique, il ne doit donc pas étendre List <>. Je suis tordu parce que vous avez des scores très élevés sur stackoverflow et que vous devriez le savoir clairement et que les gens font confiance à ce que vous dites, donc maintenant un minimum de 55 personnes qui ont voté positivement et dont l'idée est confuse croient que l'un ou l'autre est correct à construire un système mais ce n'est clairement pas le cas! # no-rage
Mauro Sampietro
6
@sam Il veut le comportement de la liste. Il a deux choix, il peut étendre la liste (héritage) ou il peut inclure une liste à l'intérieur de son objet (composition). Peut-être avez-vous mal compris une partie de la question ou de la réponse plutôt que 55 personnes qui se trompent et vous avez raison? :)
Tim B
4
Oui. C'est un cas dégénéré avec seulement un super et un sous, mais je donne l'explication plus générale de sa question spécifique. Il ne peut avoir qu'une seule équipe et qui peut toujours avoir une liste mais le choix est toujours d'hériter de la liste ou de l'inclure en tant qu'objet interne. Une fois que vous commencez à inclure plusieurs types d'équipe (football. Cricket. Etc) et qu'ils commencent à être plus qu'une simple liste de joueurs, vous voyez comment vous avez la composition complète vs la question de l'héritage. Il est important, dans des cas comme celui-ci, de voir la situation dans son ensemble pour éviter les mauvais choix tôt, ce qui signifie alors beaucoup de refactorisation plus tard.
Tim B
3
L'OP n'a pas demandé de composition, mais le cas d'utilisation est un exemple clair de problème X: Y dont l'un des 4 principes de programmation orientée objet est cassé, mal utilisé ou mal compris. La réponse la plus claire est soit d'écrire une collection spécialisée comme une pile ou une file d'attente (qui ne semble pas correspondre au cas d'utilisation) ou de comprendre la composition. Une équipe de football n'est PAS une liste de joueurs de football. Que le PO l'ait demandé explicitement ou non n'a pas d'importance, sans comprendre l'abstraction, l'encapsulation, l'héritage et le polymorphisme, ils ne comprendront pas la réponse, ergo, X: Y amok
Julia McGuigan
67

Comme tout le monde l'a souligné, une équipe de joueurs n'est pas une liste de joueurs. Cette erreur est commise par de nombreuses personnes partout dans le monde, peut-être à différents niveaux d'expertise. Souvent, le problème est subtil et parfois très grave, comme dans ce cas. De telles conceptions sont mauvaises car elles violent le principe de substitution de Liskov . Internet contient de nombreux bons articles expliquant ce concept, par exemple http://en.wikipedia.org/wiki/Liskov_substitution_principle

En résumé, il existe deux règles à conserver dans une relation parent / enfant entre les classes:

  • un enfant ne devrait exiger aucune caractéristique inférieure à ce qui définit complètement le parent.
  • un parent ne devrait exiger aucune caractéristique en plus de ce qui définit complètement l'enfant.

En d'autres termes, un parent est une définition nécessaire d'un enfant et un enfant est une définition suffisante d'un parent.

Voici une façon de réfléchir à sa solution et d'appliquer le principe ci-dessus qui devrait aider à éviter une telle erreur. On devrait tester son hypothèse en vérifiant si toutes les opérations d'une classe parente sont valides pour la classe dérivée à la fois structurellement et sémantiquement.

  • Une équipe de football est-elle une liste de joueurs de football? (Toutes les propriétés d'une liste s'appliquent-elles à une équipe dans le même sens)
    • Une équipe est-elle une collection d'entités homogènes? Oui, l'équipe est une collection de joueurs
    • L'ordre d'inclusion des joueurs est-il descriptif de l'état de l'équipe et l'équipe s'assure-t-elle que la séquence est préservée à moins qu'elle ne soit explicitement modifiée? Non et non
    • Les joueurs devraient-ils être inclus / abandonnés en fonction de leur position séquentielle dans l'équipe? Non

Comme vous le voyez, seule la première caractéristique d'une liste est applicable à une équipe. Une équipe n'est donc pas une liste. Une liste serait un détail d'implémentation de la façon dont vous gérez votre équipe, elle ne devrait donc être utilisée que pour stocker les objets des joueurs et être manipulée avec des méthodes de classe Team.

À ce stade, je voudrais faire remarquer qu'une classe Team ne devrait pas, à mon avis, même être implémentée à l'aide d'une liste; il doit être implémenté à l'aide d'une structure de données Set (HashSet, par exemple) dans la plupart des cas.

Satyan Raina
la source
8
Belle prise sur la liste contre l'ensemble. Cela semble être une erreur trop courante. Lorsqu'il ne peut y avoir qu'une seule instance d'un élément dans une collection, une sorte d'ensemble ou de dictionnaire doit être un candidat préféré pour l'implémentation. C'est bien d'avoir une équipe avec deux joueurs du même nom. Ce n'est pas bien d'avoir un seul joueur inclus deux fois en même temps.
Fred Mitchell
1
+1 pour certains bons points, bien qu'il soit plus important de demander "une structure de données peut-elle raisonnablement prendre en charge tout ce qui est utile (idéalement à court et à long terme)", plutôt que "la structure de données fait-elle plus qu'utile".
Tony Delroy
1
@TonyD Eh bien, les points que je soulève ne sont pas que l'on devrait vérifier "si la structure des données fait plus que ce qui est utile". Il s'agit de vérifier "Si la structure de données Parent fait quelque chose qui n'est pas pertinent, dénué de sens ou contre-intuitif du comportement que la classe Child pourrait impliquer".
Satyan Raina
1
@TonyD Il y a en fait un problème avec des caractéristiques non pertinentes dérivées d'un parent car cela échouerait aux tests négatifs dans de nombreux cas. Un programmeur pourrait étendre Human {eat (); courir(); write ();} d'un gorille {eat (); courir(); swing ();} pensant qu'il n'y a rien de mal à un humain avec une caractéristique supplémentaire de pouvoir se balancer. Et puis, dans un monde de jeu, votre humain commence soudainement à contourner tous les obstacles terrestres supposés en se balançant simplement au-dessus des arbres. Sauf indication contraire explicite, un humain pratique ne devrait pas être capable de se balancer. Une telle conception laisse l'API très ouverte aux abus et à la confusion
Satyan Raina
1
@TonyD Je ne suggère pas non plus que la classe Player soit dérivée de HashSet. Je suggère que la classe Player devrait `` dans la plupart des cas '' être implémentée à l'aide d'un HashSet via Composition et c'est totalement un détail de niveau d'implémentation, pas un niveau de conception (c'est pourquoi je l'ai mentionné en tant que sidenote à ma réponse). Il pourrait très bien être implémenté à l'aide d'une liste s'il existe une justification valable pour une telle implémentation. Donc, pour répondre à votre question, est-il nécessaire d'avoir une recherche O (1) par clé? Non. Par conséquent, il ne faut PAS étendre un joueur à partir d'un HashSet également.
Satyan Raina
50

Que se passe-t-il si le pays FootballTeama une équipe de réserve avec l'équipe principale?

class FootballTeam
{
    List<FootballPlayer> Players { get; set; }
    List<FootballPlayer> ReservePlayers { get; set; }
}

Comment modéliseriez-vous cela?

class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal 
}

La relation est clairement a et non est un .

ou RetiredPlayers?

class FootballTeam
{
    List<FootballPlayer> Players { get; set; }
    List<FootballPlayer> ReservePlayers { get; set; }
    List<FootballPlayer> RetiredPlayers { get; set; }
}

En règle générale, si vous souhaitez hériter d'une collection, nommez la classe SomethingCollection .

Votre SomethingCollectionsémantique a- t-elle un sens? Ne faites cela que si votre type est une collection deSomething .

Dans le cas, FootballTeamcela ne sonne pas bien. A Teamest plus qu'un Collection. A Teampeut avoir des entraîneurs, des formateurs, etc. comme l'ont indiqué les autres réponses.

FootballCollectionsonne comme une collection de ballons de football ou peut-être une collection d'accessoires de football. TeamCollection, un ensemble d'équipes.

FootballPlayerCollection sonne comme une collection de joueurs qui serait un nom valide pour une classe qui hérite de List<FootballPlayer> si vous vouliez vraiment le faire.

C'est vraiment List<FootballPlayer>un bon type à gérer. Peut êtreIList<FootballPlayer> - si vous le renvoyez d'une méthode.

En résumé

Demande toi

  1. Est X un Y? ou a X un Y?

  2. Mes noms de classe signifient-ils ce qu'ils sont?

Sam Leach
la source
Si le type de chaque joueur se classer comme appartenant à DefensivePlayers, OffensivePlayersou OtherPlayers, il peut être légitimement utile d'avoir un type qui pourrait être utilisé par le code qui attend un List<Player>mais aussi inclus les membres DefensivePlayers, OffsensivePlayersou SpecialPlayersde type IList<DefensivePlayer>, IList<OffensivePlayer>et IList<Player>. On pourrait utiliser un objet séparé pour mettre en cache les listes séparées, mais les encapsuler dans le même objet que la liste principale semblerait plus propre [utilisez l'invalidation d'un énumérateur de liste ...
supercat
... comme indice du fait que la liste principale a changé et que les sous-listes devront être régénérées lors de leur prochain accès].
supercat
2
Bien que je sois d'accord avec le point soulevé, cela me déchire vraiment l'âme de voir quelqu'un donner des conseils de conception et suggérer d'exposer une liste concrète <T> avec un getter et un setter public dans un objet métier :(
sara
38

Conception> Implémentation

Les méthodes et les propriétés que vous exposez sont une décision de conception. La classe de base dont vous héritez est un détail d'implémentation. Je pense qu'il vaut la peine de prendre du recul par rapport à l'ancien.

Un objet est une collection de données et de comportements.

Vos premières questions devraient donc être:

  • Quelles données cet objet comprend-il dans le modèle que je crée?
  • Quel comportement cet objet présente-t-il dans ce modèle?
  • Comment cela pourrait-il changer à l'avenir?

Gardez à l'esprit que l'héritage implique une relation "isa" (est une), tandis que la composition implique une relation "a une" (hasa). Choisissez celui qui convient à votre situation, en gardant à l'esprit où les choses pourraient aller à mesure que votre application évolue.

Pensez à penser dans les interfaces avant de penser aux types concrets, car certaines personnes trouvent plus facile de mettre leur cerveau en "mode conception" de cette façon.

Ce n'est pas quelque chose que tout le monde fait consciemment à ce niveau dans le codage quotidien. Mais si vous réfléchissez à ce genre de sujet, vous marchez dans les eaux du design. En être conscient peut être libérateur.

Tenez compte des spécificités de conception

Jetez un œil à List <T> et IList <T> sur MSDN ou Visual Studio. Voir quelles méthodes et propriétés ils exposent. Ces méthodes ressemblent-elles toutes à quelque chose que quelqu'un voudrait faire à une équipe de football selon vous?

FootballTeam.Reverse () a-t-il un sens pour vous? FootballTeam.ConvertAll <TOutput> () ressemble-t-il à quelque chose que vous voulez?

Ce n'est pas une question piège; la réponse pourrait être véritablement "oui". Si vous implémentez / héritez List <Player> ou IList <Player>, vous êtes coincé avec eux; si c'est idéal pour votre modèle, faites-le.

Si vous décidez oui, cela a du sens, et vous voulez que votre objet soit traitable comme une collection / liste de joueurs (comportement), et vous voulez donc implémenter ICollection ou IList, par tous les moyens. Sur le plan théorique:

class FootballTeam : ... ICollection<Player>
{
    ...
}

Si vous souhaitez que votre objet contienne une collection / liste de joueurs (données), et que vous souhaitez donc que la collection ou la liste soit une propriété ou un membre, faites-le. Sur le plan théorique:

class FootballTeam ...
{
    public ICollection<Player> Players { get { ... } }
}

Vous pourriez penser que vous voulez que les gens puissent uniquement énumérer l'ensemble des joueurs, plutôt que de les compter, de les ajouter ou de les supprimer. IEnumerable <Player> est une option parfaitement valable à considérer.

Vous pourriez penser qu'aucune de ces interfaces n'est utile du tout dans votre modèle. C'est moins probable (IEnumerable <T> est utile dans de nombreuses situations) mais c'est toujours possible.

Quiconque essaie de vous dire que l'un d'entre eux est catégoriquement et définitivement faux dans tous les cas est mal avisé. Quiconque essaie de vous le dire a catégoriquement et définitivement raison dans tous les cas est erroné.

Passez à la mise en œuvre

Une fois que vous avez décidé des données et du comportement, vous pouvez prendre une décision concernant la mise en œuvre. Cela inclut les classes concrètes dont vous dépendez via l'héritage ou la composition.

Ce n'est peut-être pas une grande étape, et les gens confondent souvent conception et implémentation car il est tout à fait possible de parcourir tout cela dans votre tête en une seconde ou deux et de commencer à taper.

Une expérience de réflexion

Un exemple artificiel: comme d'autres l'ont mentionné, une équipe n'est pas toujours "juste" un ensemble de joueurs. Maintenez-vous une collection de scores de match pour l'équipe? L'équipe est-elle interchangeable avec le club, dans votre modèle? Si c'est le cas, et si votre équipe est une collection de joueurs, c'est peut-être aussi une collection de personnel et / ou une collection de scores. Vous vous retrouvez alors avec:

class FootballTeam : ... ICollection<Player>, 
                         ICollection<StaffMember>,
                         ICollection<Score>
{
    ....
}

Nonobstant la conception, à ce stade de C #, vous ne pourrez pas implémenter tout cela en héritant de List <T> de toute façon, car C # "ne" prend en charge que l'héritage unique. (Si vous avez essayé ce malarky en C ++, vous pouvez considérer cela comme une bonne chose.) L'implémentation d'une collection via l'héritage et une via la composition est susceptible de se sentir sale. Et des propriétés telles que Count deviennent déroutantes pour les utilisateurs à moins que vous n'implémentiez explicitement ILIst <Player> .Count et IList <StaffMember> .Count etc., puis elles sont simplement douloureuses plutôt que déroutantes. Vous pouvez voir où cela va; ressentir les tripes en réfléchissant à cette avenue pourrait bien vous dire que vous vous sentez mal de vous diriger dans cette direction (et à tort ou à raison, vos collègues pourraient aussi si vous la mettiez en œuvre de cette façon!)

La réponse courte (trop tard)

La directive de ne pas hériter des classes de collection n'est pas spécifique à C #, vous la trouverez dans de nombreux langages de programmation. C'est de la sagesse reçue et non une loi. L'une des raisons est que, dans la pratique, la composition est souvent considérée comme gagnante sur l'héritage en termes de compréhensibilité, de mise en œuvre et de maintenabilité. Il est plus courant avec des objets du monde réel / domaine de trouver des relations "hasa" utiles et cohérentes que des relations "isa" utiles et cohérentes, sauf si vous êtes au fond de l'abstrait, en particulier au fil du temps et des données et du comportement précis des objets dans le code changements. Cela ne devrait pas vous obliger à toujours exclure l'héritage des classes de collection; mais cela peut être suggestif.

El Zorko
la source
34

Tout d'abord, cela concerne la convivialité. Si vous utilisez l'héritage, la Teamclasse exposera les comportements (méthodes) conçus uniquement pour la manipulation d'objets. Par exemple, AsReadOnly()ou les CopyTo(obj)méthodes n'ont aucun sens pour l'objet d'équipe. Au lieu de la AddRange(items)méthode, vous voudriez probablement une AddPlayers(players)méthode plus descriptive .

Si vous souhaitez utiliser LINQ, implémenter une interface générique telle que ICollection<T>ou IEnumerable<T>aurait plus de sens.

Comme mentionné, la composition est la bonne façon de procéder. Implémentez simplement une liste de joueurs en tant que variable privée.

Dmitry S.
la source
30

Une équipe de football n'est pas une liste de joueurs de football. Une équipe de football est composée d' une liste de joueurs de football!

C'est logiquement faux:

class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal 
}

et c'est correct:

class FootballTeam 
{ 
    public List<FootballPlayer> players
    public string TeamName; 
    public int RunningTotal 
}
Mauro Sampietro
la source
19
Cela n'explique pas pourquoi . L'OP sait que la plupart des autres programmeurs ressentent cela, il ne comprend tout simplement pas pourquoi c'est important. Ce n'est que vraiment fournir des informations déjà dans la question.
Servy
2
C'est la première chose à laquelle j'ai pensé aussi, comment ses données ont été mal modélisées. La raison en est que votre modèle de données est incorrect.
rball
3
Pourquoi ne pas hériter de la liste? Parce qu'il ne veut pas d'une liste plus précise.
Mauro Sampietro
2
Je ne pense pas que le deuxième exemple soit correct. Vous exposez l'état et rompez ainsi l'encapsulation. L'état doit être caché / interne à l'objet et la façon de muter cet état doit être via des méthodes publiques. Exactement quelles méthodes doivent être déterminées à partir des exigences de l'entreprise, mais elles devraient être sur une base "besoin de ce moment", pas "pourrait être utile quelque temps je ne sais pas" -basis. Gardez votre api serré et vous vous épargnerez du temps et des efforts.
sara
1
L'encapsulation n'est pas le point de la question. Je me concentre sur ne pas étendre un type si vous ne voulez pas que ce type se comporte d'une manière plus spécifique. Ces champs devraient être des propriétés automatiques en c # et des méthodes get / set dans d'autres langues mais ici dans ce contexte qui est totalement superflu
Mauro Sampietro
28

ça dépend du contexte

Lorsque vous considérez votre équipe comme une liste de joueurs, vous projetez "l'idée" d'une équipe de foot ball sur un seul aspect: vous réduisez "l'équipe" aux personnes que vous voyez sur le terrain. Cette projection n'est correcte que dans un certain contexte. Dans un contexte différent, cela pourrait être complètement faux. Imaginez que vous souhaitiez devenir un sponsor de l'équipe. Il faut donc parler aux managers de l'équipe. Dans ce contexte, l'équipe est projetée sur la liste de ses managers. Et ces deux listes ne se chevauchent généralement pas beaucoup. D'autres contextes sont l'actuel contre les anciens joueurs, etc.

Sémantique peu claire

Ainsi, le problème de considérer une équipe comme une liste de ses joueurs est que sa sémantique dépend du contexte et qu'elle ne peut être étendue lorsque le contexte change. De plus, il est difficile d'exprimer le contexte que vous utilisez.

Les cours sont extensibles

Lorsque vous utilisez une classe avec un seul membre (par exemple IList activePlayers ), vous pouvez utiliser le nom du membre (et en plus son commentaire) pour clarifier le contexte. Lorsqu'il existe des contextes supplémentaires, vous ajoutez simplement un membre supplémentaire.

Les cours sont plus complexes

Dans certains cas, il peut être exagéré de créer une classe supplémentaire. Chaque définition de classe doit être chargée via le chargeur de classe et sera mise en cache par la machine virtuelle. Cela vous coûte des performances d'exécution et de la mémoire. Lorsque vous avez un contexte très spécifique, il peut être judicieux de considérer une équipe de football comme une liste de joueurs. Mais dans ce cas, vous devez simplement utiliser un IList , pas une classe qui en dérive.

Conclusion / considérations

Lorsque vous avez un contexte très spécifique, il est normal de considérer une équipe comme une liste de joueurs. Par exemple, à l'intérieur d'une méthode, il est tout à fait correct d'écrire:

IList<Player> footballTeam = ...

Lorsque vous utilisez F #, il peut même être OK de créer une abréviation de type:

type FootballTeam = IList<Player>

Mais lorsque le contexte est plus large ou même peu clair, vous ne devriez pas le faire. C'est particulièrement le cas lorsque vous créez une nouvelle classe dont le contexte dans lequel elle pourra être utilisée à l'avenir n'est pas clair. Un signe d'avertissement est lorsque vous commencez à ajouter des attributs supplémentaires à votre classe (nom de l'équipe, entraîneur, etc.). C'est un signe clair que le contexte dans lequel la classe sera utilisée n'est pas fixe et changera à l'avenir. Dans ce cas, vous ne pouvez pas considérer l'équipe comme une liste de joueurs, mais vous devez modéliser la liste des joueurs (actuellement actifs, non blessés, etc.) comme un attribut de l'équipe.

stefan.schwetschke
la source
27

Permettez-moi de réécrire votre question. de sorte que vous pourriez voir le sujet sous un angle différent.

Quand je dois représenter une équipe de football, je comprends que c'est essentiellement un nom. Comme: "The Eagles"

string team = new string();

Plus tard, j'ai réalisé que les équipes avaient aussi des joueurs.

Pourquoi ne puis-je pas simplement étendre le type de chaîne afin qu'il contienne également une liste de joueurs?

Votre point d'entrée dans le problème est arbitraire. Essayez de penser que fait une équipe ont (propriétés), pas ce qu'il est .

Après cela, vous pouvez voir s'il partage des propriétés avec d'autres classes. Et pensez à l'héritage.

user1852503
la source
1
C'est un bon point - on pourrait considérer l'équipe comme un simple nom. Cependant, si ma candidature vise à travailler avec les actions des joueurs, alors cette réflexion est un peu moins évidente. Quoi qu'il en soit, il semble que le problème se résume à la composition vs l'héritage à la fin.
Superbe
6
C'est une façon méritoire de voir les choses. En note, veuillez considérer ceci: Un homme riche possède plusieurs équipes de football, il en donne une à un ami en cadeau, l'ami change le nom de l'équipe, licencie l'entraîneur, et remplace tous les joueurs. L'équipe d'amis rencontre l'équipe masculine sur le green et alors que l'équipe masculine perd, il dit "Je ne peux pas croire que vous me battez avec l'équipe que je vous ai donnée!" l'homme a-t-il raison? comment verriez-vous cela?
user1852503
20

Tout simplement parce que je pense que les autres réponses vont à peu près sur une tangente de savoir si une équipe de football "est-un" List<FootballPlayer>ou "a-un"List<FootballPlayer> , ce qui ne répond vraiment pas à cette question telle qu'elle est écrite.

Le PO demande principalement des éclaircissements sur les lignes directrices pour hériter de List<T>:

Une directive indique que vous ne devriez pas hériter de List<T>. Pourquoi pas?

Parce qu'il List<T>n'a pas de méthodes virtuelles. C'est moins un problème dans votre propre code, car vous pouvez généralement désactiver l'implémentation avec relativement peu de douleur - mais cela peut être beaucoup plus important dans une API publique.

Qu'est-ce qu'une API publique et pourquoi devrais-je m'en soucier?

Une API publique est une interface que vous exposez à des programmeurs tiers. Pensez au code cadre. Et rappelez-vous que les directives référencées sont les " Directives de conception du .NET Framework " et non les " Directives de conception de l' application .NET ". Il y a une différence et, d'une manière générale, la conception d'API publique est beaucoup plus stricte.

Si mon projet actuel ne dispose pas et n'est pas susceptible d'avoir cette API publique, puis-je ignorer cette directive en toute sécurité? Si j'hérite de List et qu'il s'avère que j'ai besoin d'une API publique, quelles difficultés vais-je rencontrer?

À peu près, oui. Vous voudrez peut-être examiner la raison d'être de cela pour voir si cela s'applique à votre situation de toute façon, mais si vous ne construisez pas une API publique, vous n'avez pas particulièrement à vous soucier des problèmes d'API comme la gestion des versions (dont, c'est un sous-ensemble).

Si vous ajoutez une API publique à l'avenir, vous devrez soit retirer votre API de votre mise en œuvre (en ne l'exposant pas List<T>directement) ou violer les directives avec la douleur future possible que cela implique.

Pourquoi est-ce même important? Une liste est une liste. Qu'est-ce qui pourrait éventuellement changer? Que pourrais-je vouloir changer?

Cela dépend du contexte, mais puisque nous l'utilisons FootballTeamcomme exemple - imaginez que vous ne pouvez pas ajouter un FootballPlayersi cela ferait dépasser le plafond salarial par l'équipe. Une façon possible d'ajouter cela serait quelque chose comme:

 class FootballTeam : List<FootballPlayer> {
     override void Add(FootballPlayer player) {
        if (this.Sum(p => p.Salary) + player.Salary > SALARY_CAP)) {
          throw new InvalidOperationException("Would exceed salary cap!");
        }
     }
 }

Ah ... mais vous ne pouvez pas override Addparce que ce n'est pas le cas virtual(pour des raisons de performances).

Si vous êtes dans une application (ce qui signifie essentiellement que vous et tous vos appelants sont compilés ensemble), vous pouvez maintenant passer à l'utilisation IList<T>et corriger les erreurs de compilation:

 class FootballTeam : IList<FootballPlayer> {
     private List<FootballPlayer> Players { get; set; }

     override void Add(FootballPlayer player) {
        if (this.Players.Sum(p => p.Salary) + player.Salary > SALARY_CAP)) {
          throw new InvalidOperationException("Would exceed salary cap!");
        }
     }
     /* boiler plate for rest of IList */
 }

mais, si vous vous êtes exposé publiquement à un tiers, vous venez de faire un changement de rupture qui entraînera des erreurs de compilation et / ou d'exécution.

TL; DR - les directives concernent les API publiques. Pour les API privées, faites ce que vous voulez.

Mark Brackett
la source
18

Est-ce que permettre aux gens de dire

myTeam.subList(3, 5);

tout sens? Sinon, ce ne devrait pas être une liste.

Cruncher
la source
3
Ça pourrait si vous l'appeliezmyTeam.subTeam(3, 5);
Sam Leach
3
@SamLeach En supposant que c'est vrai, alors vous aurez toujours besoin de composition et non d'héritage. Comme subListne reviendra même Teamplus.
Cruncher
1
Cela m'a convaincu plus que toute autre réponse. +1
FireCubez
16

Cela dépend du comportement de votre objet "équipe". S'il se comporte exactement comme une collection, il peut être correct de le représenter d'abord avec une liste simple. Ensuite, vous pourriez commencer à remarquer que vous continuez à dupliquer du code qui itère dans la liste; à ce stade, vous avez la possibilité de créer un objet FootballTeam qui enveloppe la liste des joueurs. La classe FootballTeam devient le foyer de tout le code qui itère sur la liste des joueurs.

Cela rend mon code inutilement verbeux. Je dois maintenant appeler my_team.Players.Count au lieu de simplement my_team.Count. Heureusement, avec C # je peux définir des indexeurs pour rendre l'indexation transparente, et transmettre toutes les méthodes de la liste interne ... Mais c'est beaucoup de code! Qu'est-ce que j'obtiens pour tout ce travail?

Encapsulation. Vos clients n'ont pas besoin de savoir ce qui se passe à l'intérieur de FootballTeam. Pour tous vos clients savent, il peut être mis en œuvre en recherchant la liste des joueurs dans une base de données. Ils n'ont pas besoin de le savoir, ce qui améliore votre conception.

Cela n'a tout simplement aucun sens. Une équipe de football n'a pas de liste de joueurs. C'est la liste des joueurs. Vous ne dites pas "John McFootballer a rejoint les joueurs de SomeTeam". Vous dites "John a rejoint SomeTeam". Vous n'ajoutez pas de lettre aux "caractères d'une chaîne", vous ajoutez une lettre à une chaîne. Vous n'ajoutez pas un livre aux livres d'une bibliothèque, vous ajoutez un livre à une bibliothèque.

Exactement :) vous direz footballTeam.Add (john), pas footballTeam.List.Add (john). La liste interne ne sera pas visible.

xpmatteo
la source
1
OP veut comprendre comment MODELISER LA REALITE, mais définit ensuite une équipe de football comme une liste de joueurs de football (ce qui est faux conceptuellement). C'est le problème. À mon avis, tout autre argument le trompe.
Mauro Sampietro
3
Je ne suis pas d'accord que la liste des joueurs soit conceptuellement erronée. Pas nécessairement. Comme quelqu'un d'autre l'a écrit dans cette page, "Tous les modèles sont faux, mais certains sont utiles"
xpmatteo
Les bons modèles sont difficiles à obtenir, une fois terminés, ils sont ceux qui sont utiles. Si un modèle peut être mal utilisé, il est faux conceptuellement. stackoverflow.com/a/21706411/711061
Mauro Sampietro
15

Il y a beaucoup d'excellentes réponses ici, mais je veux aborder quelque chose que je n'ai pas vu mentionné: la conception orientée objet concerne l' autonomisation des objets .

Vous souhaitez encapsuler toutes vos règles, travaux supplémentaires et détails internes dans un objet approprié. De cette façon, les autres objets qui interagissent avec celui-ci n'ont pas à se soucier de tout cela. En fait, vous voulez aller plus loin et empêcher activement d' autres objets de contourner ces éléments internes.

Lorsque vous héritez de List , tous les autres objets peuvent vous voir comme une liste. Ils ont un accès direct aux méthodes d'ajout et de suppression de joueurs. Et vous aurez perdu votre contrôle; par exemple:

Supposons que vous vouliez faire la différence quand un joueur part en sachant s'il a pris sa retraite, démissionné ou été licencié. Vous pouvez implémenter une RemovePlayerméthode qui prend une énumération d'entrée appropriée. Cependant, en héritant de List, vous ne pourrez pas empêcher l'accès direct à Remove, RemoveAllet même Clear. En conséquence, vous avez en fait mis hors de pouvoir votre FootballTeamclasse.


Réflexions supplémentaires sur l'encapsulation ... Vous avez soulevé la préoccupation suivante:

Cela rend mon code inutilement verbeux. Je dois maintenant appeler my_team.Players.Count au lieu de simplement my_team.Count.

Vous avez raison, ce serait inutilement verbeux pour tous les clients d'utiliser votre équipe. Cependant, ce problème est très faible par rapport au fait que vous avez exposéList Players à tout le monde afin qu'ils puissent jouer avec votre équipe sans votre consentement.

Vous continuez en disant:

Cela n'a tout simplement aucun sens. Une équipe de football ne "possède" pas de liste de joueurs. C'est la liste des joueurs. Vous ne dites pas "John McFootballer a rejoint les joueurs de SomeTeam". Vous dites "John a rejoint SomeTeam".

Vous vous trompez sur le premier morceau: supprimez le mot «liste», et il est en fait évident qu'une équipe a des joueurs.
Cependant, vous frappez le clou sur la tête avec le second. Vous ne voulez pas que les clients appellent ateam.Players.Add(...). Vous voulez qu'ils appellent ateam.AddPlayer(...). Et votre implémentation s'appellerait (peut-être entre autres) en Players.Add(...)interne.


J'espère que vous pouvez voir à quel point l'encapsulation est importante pour l'objectif d'autonomisation de vos objets. Vous voulez permettre à chaque classe de bien faire son travail sans crainte d'interférence avec d'autres objets.

Désabusé
la source
12

Quelle est la bonne façon C # de représenter une structure de données ...

Rappelez-vous, "Tous les modèles sont faux, mais certains sont utiles." - Boîte George EP

Il n'y a pas de "bonne façon", seulement utile.

Choisissez celui qui vous est utile et / et vos utilisateurs. C'est ça. Développez-vous économiquement, ne faites pas d'ingénierie excessive. Moins vous écrivez de code, moins vous aurez besoin de déboguer. (lire les éditions suivantes).

- Modifié

Ma meilleure réponse serait ... ça dépend. L'héritage d'une liste exposerait les clients de cette classe à des méthodes qui ne devraient pas être exposées, principalement parce que FootballTeam ressemble à une entité commerciale.

- Édition 2

Je ne me souviens sincèrement pas de ce à quoi je faisais référence sur le commentaire «ne pas trop ingénieur». Bien que je pense que l' état d'esprit KISS est un bon guide, je tiens à souligner que l'héritage d'une classe affaires de List créerait plus de problèmes qu'il n'en résout, en raison d'une fuite d'abstraction .

D'un autre côté, je pense qu'il y a un nombre limité de cas où simplement hériter de List est utile. Comme je l'ai écrit dans l'édition précédente, cela dépend. La réponse à chaque cas est fortement influencée par les connaissances, l'expérience et les préférences personnelles.

Merci à @kai de m'avoir aidé à réfléchir plus précisément à la réponse.

ArturoTena
la source
Ma meilleure réponse serait ... ça dépend. L'héritage d'une liste exposerait les clients de cette classe à des méthodes qui ne devraient pas être exposées, principalement parce que FootballTeam ressemble à une entité commerciale. Je ne sais pas si je dois modifier cette réponse ou en poster une autre, une idée?
ArturoTena
2
Le bit sur " hériter d'un Listexposerait les clients de cette classe à des méthodes qui ne devraient pas être exposées " est précisément ce qui rend héritant d' Listun non-non absolu. Une fois exposés, ils sont progressivement maltraités et mal utilisés au fil du temps. Gagner du temps maintenant en "développant économiquement" peut facilement décupler les économies perdues à l'avenir: débogage des abus et éventuellement refactoring pour réparer l'héritage. Le principe YAGNI peut également être considéré comme signifiant: vous n'aurez pas besoin de toutes ces méthodes List, alors ne les exposez pas.
Désillusionné le
3
"Ne faites pas d'ingénierie excessive. Moins vous écrivez de code, moins vous aurez besoin de déboguer." <- Je pense que cela est trompeur. L'encapsulation et la composition sur l'héritage ne sont PAS une ingénierie excessive et ne nécessitent pas plus de débogage. En encapsulant, vous LIMITEZ le nombre de façons dont les clients peuvent utiliser (et abuser) de votre classe et donc vous avez moins de points d'entrée qui nécessitent des tests et une validation d'entrée. Hériter de Listparce que c'est rapide et facile et entraînerait donc moins de bugs est tout simplement faux, c'est juste une mauvaise conception et une mauvaise conception conduit à beaucoup plus de bugs que "over engineering".
sara
@kai, je suis d'accord avec vous sur tous les points. Je ne me souviens sincèrement pas de ce à quoi je faisais référence sur le commentaire «ne pas trop ingénieur». OTOH, je crois qu'il y a un nombre limité de cas où simplement hériter de List est utile. Comme je l'ai écrit dans la dernière édition, cela dépend. La réponse à chaque cas est fortement influencée par les connaissances, l'expérience et les préférences personnelles. Comme tout dans la vie. ;-)
ArturoTena
11

Cela me rappelle le "est un" contre "a un" compromis. Parfois, c'est plus facile et plus logique d'hériter directement d'une super classe. D'autres fois, il est plus logique de créer une classe autonome et d'inclure la classe dont vous auriez hérité en tant que variable membre. Vous pouvez toujours accéder aux fonctionnalités de la classe, mais n'êtes pas lié à l'interface ni à aucune autre contrainte pouvant provenir de l'héritage de la classe.

Que faites-vous? Comme pour beaucoup de choses ... cela dépend du contexte. Le guide que j'utiliserais est que pour hériter d'une autre classe, il devrait vraiment y avoir une relation "est une". Donc, si vous écrivez une classe appelée BMW, elle pourrait hériter de Car car une BMW est vraiment une voiture. Une classe Cheval peut hériter de la classe Mammifère car un cheval est en réalité un mammifère dans la vraie vie et toute fonctionnalité Mammifère doit être pertinente pour le Cheval. Mais pouvez-vous dire qu'une équipe est une liste? D'après ce que je peux dire, il ne semble pas qu'une équipe soit vraiment "une" liste. Donc, dans ce cas, j'aurais une liste en tant que variable membre.

Paul J Abernathy
la source
9

Mon sale secret: je me fiche de ce que les gens disent et je le fais. .NET Framework est diffusé avec "XxxxCollection" (UIElementCollection pour le haut de mon exemple).

Alors, ce qui m'empêche de dire:

team.Players.ByName("Nicolas")

Quand je le trouve mieux que

team.ByName("Nicolas")

De plus, ma PlayerCollection peut être utilisée par une autre classe, comme "Club" sans aucune duplication de code.

club.Players.ByName("Nicolas")

Les bonnes pratiques d'hier ne sont peut-être pas celles de demain. Il n'y a aucune raison derrière la plupart des meilleures pratiques, la plupart ne sont que largement acceptées par la communauté. Au lieu de demander à la communauté si cela vous blâmera lorsque vous le ferez, demandez-vous quoi de plus lisible et maintenable?

team.Players.ByName("Nicolas") 

ou

team.ByName("Nicolas")

Vraiment. Avez-vous un doute? Maintenant, vous devez peut-être jouer avec d'autres contraintes techniques qui vous empêchent d'utiliser List <T> dans votre cas d'utilisation réel. Mais n'ajoutez pas une contrainte qui ne devrait pas exister. Si Microsoft n'a pas documenté le pourquoi, alors c'est sûrement une "meilleure pratique" venant de nulle part.

Nicolas Dorier
la source
9
S'il est important d'avoir le courage et l'initiative de remettre en question la sagesse acceptée lorsque cela est approprié, je pense qu'il est sage de comprendre d'abord pourquoi la sagesse acceptée a fini par être acceptée en premier lieu, avant de se lancer dans la contestation. -1 parce que "Ignorez cette directive!" n'est pas une bonne réponse à "Pourquoi cette directive existe-t-elle?" Soit dit en passant, la communauté ne s'est pas seulement "blâmée" dans ce cas, mais a fourni une explication satisfaisante qui a réussi à me convaincre.
Superbest
3
C'est en fait, quand aucune explication n'est faite, votre seul moyen est de repousser les limites et de tester sans avoir peur de l'infraction. Maintenant. Demandez-vous, même si List <T> n'était pas une bonne idée. Quelle serait la peine de changer l'héritage de List <T> à Collection <T>? Une supposition: moins de temps que de poster sur le forum, quelle que soit la longueur de votre code avec des outils de refactoring. Maintenant, c'est une bonne question, mais pas pratique.
Nicolas Dorier
Pour clarifier: je n'attends certainement pas la bénédiction de SO pour hériter de tout ce que je veux comme je le veux, mais le but de ma question était de comprendre les considérations qui devraient entrer dans cette décision, et pourquoi tant de programmeurs expérimentés semblent avoir a décidé le contraire de ce que j'ai fait. Collection<T>n'est pas le même que List<T>cela peut donc nécessiter un peu de travail pour vérifier dans un projet de grande taille.
Superbe le
2
team.ByName("Nicolas")signifie "Nicolas"est le nom de l'équipe.
Sam Leach
1
"n'ajoutez pas une contrainte qui ne devrait pas exister" Au contraire, n'exposez rien que vous n'avez pas de bonnes raisons d'exposer. Ce n'est pas du purisme ou du fanatisme aveugle, ni de la dernière tendance de la mode. C'est la conception orientée objet de base 101, niveau débutant. Ce n'est pas un secret pourquoi cette "règle" existe, c'est le résultat de plusieurs décennies d'expérience.
sara
8

Ce que les directives disent, c'est que l'API publique ne doit pas révéler la décision de conception interne de savoir si vous utilisez une liste, un ensemble, un dictionnaire, un arbre ou autre. Une «équipe» n'est pas nécessairement une liste. Vous pouvez l'implémenter sous forme de liste, mais les utilisateurs de votre API publique doivent utiliser votre classe selon les besoins. Cela vous permet de modifier votre décision et d'utiliser une structure de données différente sans affecter l'interface publique.

QuentinUK
la source
Rétrospectivement, après l'explication de @EricLippert et d'autres, vous avez en fait donné une excellente réponse pour la partie API de ma question - en plus de ce que vous avez dit, si je le fais class FootballTeam : List<FootballPlayers>, les utilisateurs de ma classe pourront dire que je '' ai hérité de List<T>en utilisant la réflexion, voir les List<T>méthodes qui ne font pas de sens pour une équipe de football, et d' être en mesure d'utiliser FootballTeamdans List<T>, donc j'avoir des détails révélateurs de mise en œuvre au client (inutilement).
Superbest
7

Quand ils disent List<T>est "optimisé", je pense qu'ils veulent dire qu'il n'a pas de fonctionnalités comme les méthodes virtuelles qui sont un peu plus chères. Le problème est donc qu'une fois que vous exposez List<T>dans votre API publique , vous perdez la possibilité d'appliquer des règles métier ou de personnaliser ses fonctionnalités plus tard. Mais si vous utilisez cette classe héritée comme interne dans votre projet (par opposition à potentiellement exposée à des milliers de vos clients / partenaires / autres équipes en tant qu'API), cela peut être OK si cela vous fait gagner du temps et c'est la fonctionnalité que vous souhaitez dupliquer. L'avantage d'hériter deList<T> est que vous éliminez beaucoup de code wrapper stupide qui ne sera jamais personnalisé dans un avenir prévisible. Aussi, si vous voulez que votre classe ait explicitement la même sémantique queList<T> pour la durée de vie de vos API, cela peut également être OK.

Je vois souvent beaucoup de gens faire des tonnes de travail supplémentaire simplement parce que la règle FxCop le dit ou que le blog de quelqu'un dit que c'est une "mauvaise" pratique. Plusieurs fois, cela transforme le code pour concevoir l'étrangeté du motif de palooza. Comme pour beaucoup de directives, traitez-les comme des directives qui peuvent avoir des exceptions.

Shital Shah
la source
4

Bien que je n'ai pas de comparaison complexe comme la plupart de ces réponses, je voudrais partager ma méthode pour gérer cette situation. En étendant IEnumerable<T>, vous pouvez autoriser votre Teamclasse à prendre en charge les extensions de requête Linq, sans exposer publiquement toutes les méthodes et propriétés de List<T>.

class Team : IEnumerable<Player>
{
    private readonly List<Player> playerList;

    public Team()
    {
        playerList = new List<Player>();
    }

    public Enumerator GetEnumerator()
    {
        return playerList.GetEnumerator();
    }

    ...
}

class Player
{
    ...
}
Null511
la source
3

Si les utilisateurs de votre classe ont besoin de toutes les méthodes et propriétés dont dispose ** List, vous devez en dériver votre classe. S'ils n'en ont pas besoin, joignez la liste et créez des enveloppes pour les méthodes dont vos utilisateurs de classe ont réellement besoin.

Il s'agit d'une règle stricte, si vous écrivez une API publique ou tout autre code qui sera utilisé par de nombreuses personnes. Vous pouvez ignorer cette règle si vous avez une petite application et pas plus de 2 développeurs. Cela vous fera gagner du temps.

Pour les petites applications, vous pouvez également envisager de choisir une autre langue, moins stricte. Ruby, JavaScript - tout ce qui vous permet d'écrire moins de code.

Ivan Nikitin
la source
3

Je voulais juste ajouter que Bertrand Meyer, l'inventeur d'Eiffel et du design par contrat, aurait Teamhérité de List<Player>sans même se battre la paupière.

Dans son livre, Object-Oriented Software Construction , il discute de l'implémentation d'un système GUI où les fenêtres rectangulaires peuvent avoir des fenêtres enfants. Il a simplement Windowhérité des deux Rectangleet Tree<Window>réutilise l'implémentation.

Cependant, C # n'est pas Eiffel. Ce dernier prend en charge l'héritage multiple et le changement de nom des fonctionnalités . En C #, lorsque vous sous-classe, vous héritez à la fois de l'interface et de l'implémentation. Vous pouvez remplacer l'implémentation, mais les conventions d'appel sont copiées directement à partir de la superclasse. Dans Eiffel, cependant, vous pouvez modifier les noms des méthodes publiques, vous pouvez donc renommer Addet Removevers Hireet Firedans votre Team. Si une instance de Teamest retransmise à List<Player>, l'appelant l'utilisera Addet la Removemodifiera, mais vos méthodes virtuelles Hireet Fireseront appelées.

Alexey
la source
1
Ici, le problème n'est pas l'héritage multiple, les mixins (etc.) ou comment gérer leur absence. Dans n'importe quelle langue, même en langage humain, une équipe n'est pas une liste de personnes mais composée d'une liste de personnes. Ceci est un concept abstrait. Si Bertrand Meyer ou quiconque gère une équipe en sous-classant List se trompe. Vous devez sous-classer une liste si vous voulez un type de liste plus spécifique. J'espère que vous êtes d'accord.
Mauro Sampietro
1
Cela dépend de ce que l'héritage est pour vous. En soi, c'est une opération abstraite, cela ne signifie pas que deux classes sont dans une relation "est un type spécial", même si c'est l'interprétation dominante. Cependant, l'héritage peut être traité comme un mécanisme de réutilisation de l'implémentation si la conception du langage le prend en charge, même si la composition est l'alternative préférée de nos jours.
Alexey
2

Je pense que je ne suis pas d'accord avec votre généralisation. Une équipe n'est pas seulement une collection de joueurs. Une équipe a tellement plus d'informations à ce sujet - nom, emblème, collection de personnel de gestion / d'administration, collection d'équipage d'entraîneurs, puis collection de joueurs. Donc, correctement, votre classe FootballTeam devrait avoir 3 collections et ne pas être elle-même une collection; si c'est pour bien modéliser le monde réel.

Vous pouvez envisager une classe PlayerCollection qui, comme Specialized StringCollection, offre d'autres fonctionnalités - comme la validation et les vérifications avant l'ajout ou la suppression d'objets dans le magasin interne.

Peut-être, la notion de parieurs PlayerCollection convient à votre approche préférée?

public class PlayerCollection : Collection<Player> 
{ 
}

Et puis la FootballTeam peut ressembler à ceci:

public class FootballTeam 
{ 
    public string Name { get; set; }
    public string Location { get; set; }

    public ManagementCollection Management { get; protected set; } = new ManagementCollection();

    public CoachingCollection CoachingCrew { get; protected set; } = new CoachingCollection();

    public PlayerCollection Players { get; protected set; } = new PlayerCollection();
}
Eniola
la source
2

Préférez les interfaces aux classes

Les classes doivent éviter de dériver des classes et implémenter à la place les interfaces minimales nécessaires.

L'héritage rompt l'encapsulation

La dérivation des classes interrompt l'encapsulation :

  • expose les détails internes sur la façon dont votre collection est implémentée
  • déclare une interface (ensemble de fonctions et propriétés publiques) qui peut ne pas être appropriée

Cela rend entre autres plus difficile la refonte de votre code.

Les classes sont un détail d'implémentation

Les classes sont un détail d'implémentation qui doit être caché aux autres parties de votre code. En bref, a System.Listest une implémentation spécifique d'un type de données abstrait, qui peut ou non être approprié maintenant et à l'avenir.

Sur le plan conceptuel, le fait que le System.Listtype de données soit appelé "liste" est un peu déroutant. A System.List<T>est une collection ordonnée modifiable qui prend en charge les opérations O (1) amorties pour ajouter, insérer et supprimer des éléments, et O (1) pour récupérer le nombre d'éléments ou obtenir et définir un élément par index.

Plus l'interface est petite, plus le code est flexible

Lors de la conception d'une structure de données, plus l'interface est simple, plus le code est flexible. Regardez la puissance de LINQ pour en faire la démonstration.

Comment choisir les interfaces

Lorsque vous pensez à "lister", vous devriez commencer par vous dire: "J'ai besoin de représenter une collection de joueurs de baseball". Supposons donc que vous décidiez de modéliser cela avec une classe. Ce que vous devez faire en premier est de décider de la quantité minimale d'interfaces que cette classe devra exposer.

Quelques questions qui peuvent aider à guider ce processus:

  • Dois-je avoir le décompte? Sinon, envisagez de mettre en œuvreIEnumerable<T>
  • Cette collection va-t-elle changer après son initialisation? Si ce n'est pas le cas IReadonlyList<T>.
  • Est-il important que je puisse accéder aux éléments par index? ConsidérerICollection<T>
  • L'ordre dans lequel j'ajoute des articles à la collection est-il important? C'est peut-être un ISet<T>?
  • Si vous voulez vraiment ces choses, allez-y et mettez-les en œuvre IList<T>.

De cette façon, vous ne couplerez pas d'autres parties du code aux détails d'implémentation de votre collection de joueurs de baseball et serez libre de changer la façon dont il est implémenté tant que vous respectez l'interface.

En adoptant cette approche, vous constaterez que le code devient plus facile à lire, à refactoriser et à réutiliser.

Notes pour éviter la plaque de chaudière

L'implémentation d'interfaces dans un IDE moderne devrait être facile. Faites un clic droit et choisissez "Implémenter l'interface". Transférez ensuite toutes les implémentations à une classe membre si vous en avez besoin.

Cela dit, si vous trouvez que vous écrivez beaucoup de passe-partout, c'est potentiellement parce que vous exposez plus de fonctions que vous ne devriez l'être. C'est la même raison pour laquelle vous ne devriez pas hériter d'une classe.

Vous pouvez également concevoir des interfaces plus petites qui conviennent à votre application, et peut-être juste quelques fonctions d'extension d'assistance pour mapper ces interfaces à toutes les autres dont vous avez besoin. C'est l'approche que j'ai adoptée dans ma propre IArrayinterface pour la bibliothèque LinqArray .

cdiggins
la source
2

Problèmes de sérialisation

Un aspect manque. Les classes qui héritent de List <> ne peuvent pas être sérialisées correctement à l' aide de XmlSerializer . Dans ce cas, DataContractSerializer doit être utilisé à la place, ou une implémentation de sérialisation propre est nécessaire.

public class DemoList : List<Demo>
{
    // using XmlSerializer this properties won't be seralized:
    string AnyPropertyInDerivedFromList { get; set; }     
}

public class Demo
{
    // this properties will be seralized
    string AnyPropetyInDemo { get; set; }  
}

Lectures complémentaires: Lorsqu'une classe est héritée de List <>, XmlSerializer ne sérialise pas d'autres attributs

marécage
la source
0

Quand est-ce acceptable?

Pour citer Eric Lippert:

Lorsque vous créez un mécanisme qui étend le List<T>mécanisme.

Par exemple, vous en avez assez de l'absence de la AddRangeméthode dans IList<T>:

public interface IMoreConvenientListInterface<T> : IList<T>
{
    void AddRange(IEnumerable<T> collection);
}

public class MoreConvenientList<T> : List<T>, IMoreConvenientListInterface<T> { }
phoog
la source