web-dev-qa-db-fra.com

Pourquoi ne pas hériter de la liste <T>?

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

Une équipe de football est juste 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 listés dans la liste.

Mais je me rends compte plus tard que les équipes ont aussi 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 cette saison, le budget actuel, les couleurs de l’uniforme, un string représentant le nom de l’équipe, etc.

Alors je pense:

OK, une équipe de football est comme une liste de joueurs, mais en plus, elle porte un nom (un string) et un total cumulé de scores (un int). .NET ne fournit pas de classe pour stocker 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>, je vais donc en hériter:

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

Mais il s'avère que ne directive indique que vous ne devriez pas hériter de List<T> . Cette ligne directrice me laisse complètement perplexe à deux égards.

Pourquoi pas?

Apparemment List est en quelque sorte optimisé pour la performance . Comment? Quels problèmes de performances vais-je causer si j'étends List? Qu'est-ce qui va se casser?

Une autre raison que j’ai vue est que List est fourni par Microsoft et que je n’ai aucun contrôle sur celui-ci, donc je ne peux pas le modifier 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 dispose pas et ne sera probablement jamais doté de cette API publique, puis-je ignorer en toute sécurité cette recommandation? Si j'hérite de List et il s'avère qu'il me faut une API publique, quelles sont les difficultés rencontrées?

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 je hérite de List, pourquoi n’ont-ils pas fait la classe sealed?

Quoi d'autre suis-je censé utiliser?

Apparemment, pour les collections personnalisées, Microsoft a fourni une classe Collection qui devrait être étendue au lieu de List. Mais cette classe est très dépouillée et n'a pas beaucoup de choses utiles, comme AddRange , par exemple. La réponse de jvitor8 fournit une justification de performance pour cette méthode particulière, mais comment un processus lent AddRange n'est-il pas meilleur qu'un non AddRange?

Hériter de Collection représente beaucoup plus de travail que d'hériter de List, et je ne vois aucun avantage. Certes, Microsoft ne me dirait pas de faire du travail supplémentaire sans raison, alors je ne peux pas m'empêcher de penser que je ne comprends pas bien quelque chose, et hériter de Collection n'est en fait pas la solution à mon problème.

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

Enfin, certains suggèrent d’emballer la List dans quelque chose:

class FootballTeam 
{ 
    public List<FootballPlayer> Players; 
}

Il y a deux problèmes avec ceci:

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

  2. Cela n'a tout simplement aucun sens. Une équipe de football ne dispose pas d'une 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 de livre aux livres d'une bibliothèque, vous ajoutez un livre à une bibliothèque.

Je réalise 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é)

Quelle est la bonne manière de représenter une structure de données en C #, ce qui, "logiquement" (c'est-à-dire "à l'esprit humain"), n'est qu'un list sur things avec quelques clochettes ?

L'héritage de List<T> est-il toujours inacceptable? Quand est-ce acceptable? Pourquoi pourquoi pas? Qu'est-ce qu'un programmeur doit prendre en compte lorsqu'il décide d'hériter de List<T> ou non?

1247
Superbest

Il y a quelques bonnes réponses ici. J'y ajouterais les points suivants.

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

Demandez à dix non-programmeurs connaissant l’existence du football de combler le vide:

A football team is a particular kind of _____

Quelqu'un a-t-il déclaré "liste des joueurs de football avec quelques cloches et sifflets" ou a-t-il 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 métier , c’est-à-dire un objet représentant un concept se trouvant dans le domaine commercial . du programme. Ne mélangez pas ceux-ci! Une équipe de football est une sorte d’équipe ; il a un alignement , un alignement est une liste de joueurs . Une liste de joueurs n'est pas un type particulier de liste de joueurs . Une liste est une liste de joueurs. Créez donc une propriété appelée Roster qui est un List<Player>. Et faites-en ReadOnlyList<Player> pendant que vous y êtes, à moins que vous ne croyiez que tous ceux qui connaissent l'existence d'une équipe de football ont la possibilité de supprimer des joueurs de la liste.

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

Inacceptable pour qui? Moi? Non.

Quand est-ce acceptable?

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

Qu'est-ce qu'un programmeur doit prendre en compte lorsqu'il décide d'hériter de List<T> ou non?

Est-ce que je construis un mécanisme ou un objet métier ?

Mais ça fait beaucoup de code! Qu'est-ce que je reçois pour tout ce travail?

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

MISE À JOUR

J'y ai réfléchi un peu plus et il y a une autre raison de ne pas modéliser une équipe de football comme une liste de joueurs. En fait, il pourrait être une mauvaise idée de modéliser une équipe de football avec 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 cas pour cette classe, mais si j'avais une classe représentant une équipe de football, je voudrais lui poser des questions du type "combien de joueurs des Seahawks ont raté des matchs à cause d'une blessure entre 2003 et 2013?" ou "Quel joueur de Denver qui jouait auparavant pour une autre équipe a enregistré la plus forte augmentation en glissement annuel de son nombre de yards courus?" 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é, retiré, etc. De toute évidence, la liste actuelle des joueurs est un fait important qui devrait probablement être au centre des préoccupations, mais il y a peut-être d'autres choses intéressantes que vous souhaitez faire avec cet objet et qui nécessitent une perspective plus historique.

1417
Eric Lippert

Enfin, certains suggèrent d’envelopper la liste dans quelque chose:

C'est la bonne façon. "Inutilement verbeux" est une mauvaise façon de regarder cela. Cela a un sens explicite lorsque vous écrivez my_team.Players.Count. Vous voulez compter les joueurs.

my_team.Count

..ne signifie rien. Compter quoi?

Une équipe n'est pas une liste, c'est plus qu'une simple liste de joueurs. Une équipe possède des joueurs, les joueurs doivent donc en faire partie (un membre).

Si vous êtes vraiment inquiet que ce soit trop verbeux, vous pouvez toujours exposer les propriétés de l'équipe:

public int PlayerCount {
    get {
        return Players.Count;
    }
}

..qui devient:

my_team.PlayerCount

Cela a maintenant un sens et adhère à The Law Of Demeter .

Vous devez également envisager de vous conformer au principe de réutilisation des composites . En héritant de List<T>, vous dites qu'une équipe est une liste de joueurs et en expose les méthodes inutiles. C'est inexact - comme vous l'avez dit, une équipe est plus qu'une liste de joueurs: elle a un nom, des directeurs, des membres du conseil d'administration, des entraîneurs, du personnel médical, des plafonds de salaire, etc. Si votre classe par équipe contient une liste de joueurs, Disons "Une équipe a une liste de joueurs", mais elle peut aussi avoir d'autres choses.

274
Simon Whitehead

Wow, votre message contient toute une série de questions et de points. La plupart des raisonnements que vous obtenez de Microsoft sont parfaitement pertinents. Commençons par tout ce qui concerne List<T>

  • List<T> est hautement optimisé. Son utilisation principale est d'être utilisé en tant que membre privé d'un objet.
  • Microsoft ne l'a pas scellé, car il peut arriver que vous souhaitiez créer une classe portant un nom plus convivial: class MyList<T, TX> : List<CustomObject<T, Something<TX>> { ... }. Maintenant, c'est aussi simple que de faire var list = new MyList<int, string>();.
  • CA1002: N'exposez pas les listes génériques : En gros, même si vous envisagez d'utiliser cette application en tant que développeur unique, il est intéressant de développer de bonnes pratiques de codage afin qu'elles soient instillées en vous et en tant que seconde nature. Vous êtes toujours autorisé à exposer la liste en tant que IList<T> si vous avez besoin qu'un consommateur ait une liste indexée. Cela vous permet de modifier l’implémentation au sein d’une classe ultérieurement.
  • Microsoft a rendu Collection<T> très générique car il s’agit d’un concept générique… le nom dit tout; c'est juste une collection. Il existe des versions plus précises telles que SortedCollection<T>, ObservableCollection<T>, ReadOnlyCollection<T>, etc., chacune implémentant 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 le fait pas.
  • La dernière partie de votre question est parfaite. Une équipe de football est plus qu'une simple liste de joueurs, il devrait donc s'agir d'une classe contenant 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.
}
140
m-y
class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal;
}

Le code précédent signifie: un groupe d’hommes de la rue qui jouent au football et qui se font appeler. Quelque chose comme:

People playing football

Quoi qu'il en soit, ce code (de la réponse de m-y)

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; }
    }

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

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

Moyens: il s’agit d’une équipe de football composée de membres de la direction, de joueurs, d’administrateurs, etc.

Image showing members (and other attributes) of the Manchester United team

Voici comment votre logique est présentée en images ...

131
Nean Der Thal

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

Dans ce cas particulier:

L’équipe est-elle une liste de joueurs avec un comportement ajouté

ou

Est-ce que l’équipe est un objet qui contient une liste de joueurs?.

En étendant la liste, vous vous limitez de plusieurs manières:

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

  2. Qu'advient-il si vous voulez avoir des listes d'autres choses aussi. Par exemple, les équipes ont des entraîneurs, des gérants, des supporters, du matériel, etc. Certaines d’entre elles pourraient bien être des listes à part entière.

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

Composition - comprenant un objet donnant le comportement souhaité dans votre objet.

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

Les deux ont leurs utilisations, mais il s'agit clairement d'un cas où la composition est préférable.

103
Tim B

Comme tout le monde l’a fait remarquer, 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. Le problème est souvent subtil et parfois très grossier, comme dans le cas présent. De telles conceptions sont mauvaises car elles violent le principe de substitution de Liskov . Internet contient de nombreux articles expliquant ce concept, par exemple, http://en.wikipedia.org/wiki/Liskov_substitution_principle

En résumé, deux règles doivent être préservées dans une relation parent/enfant parmi les classes:

  • un enfant ne devrait exiger aucune caractéristique inférieure à celle 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 à une solution et d'appliquer le principe ci-dessus qui devrait aider à éviter une telle erreur. Il convient de tester son hypothèse en vérifiant si toutes les opérations d'une classe parente sont valables pour la classe dérivée structurellement et sémantiquement.

  • Une équipe de football est-elle une liste de joueurs de football? (Est-ce que toutes les propriétés d'une liste s'appliquent à une équipe dans le même sens)
    • Une équipe est-elle un ensemble 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 sauf si elle est explicitement modifiée? Non et non
    • Les joueurs sont-ils censés ê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. Par conséquent, une équipe n'est 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 du joueur et être manipulée avec les méthodes de classe Team.

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

62
Satyan Raina

Que se passe-t-il si la FootballTeam a une équipe de réserve avec l'équipe principale?

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

Comment modéliserais-tu cela?

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

La relation est clairement a un 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 SomethingCollection a-t-il un sens sur le plan sémantique? Ne le faites que si votre type est une collection de Something.

Dans le cas de FootballTeam cela ne sonne pas bien. Un Team est plus qu'un Collection. Un Team peut avoir des entraîneurs, des formateurs, etc. comme l'ont souligné les autres réponses.

FootballCollection ressemble à une collection de ballons de football ou peut-être à une collection d'attirail de football. TeamCollection, un ensemble d'équipes.

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

Vraiment List<FootballPlayer> est un type parfaitement approprié. Peut-être que IList<FootballPlayer> si vous le retournez à partir d'une méthode.

en résumé

Demande toi

  1. Est-ce que X a Y? ou A X a Y?

  2. Est-ce que mes noms de classe veulent dire ce qu'ils sont?

46
Sam Leach

Design> Implémentation

Quelles méthodes et propriétés vous exposez est une décision de conception. La classe de base dont vous héritez est un détail d'implémentation. Je pense que cela vaut la peine de revenir en arrière.

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

Donc, vos premières questions devraient ê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), alors que la composition implique une relation "a une" (hasa). Choisissez celui qui convient le mieux à votre situation, en gardant à l’esprit que les choses peuvent aller au fur et à mesure que votre application évolue.

Réfléchissez aux 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 avancez dans les eaux du design. En être conscient peut être libérateur.

Considérer les détails de conception

Consultez Liste <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 à votre avis?

FootballTeam.Reverse () a-t-il un sens pour vous? FootballTeam.ConvertAll <TOutput> () a-t-il l'air de quelque chose que vous voulez?

Ce n'est pas une question piège; la réponse pourrait vraiment être "oui". Si vous implémentez/héritez de 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 si vous voulez que votre objet puisse être traité comme une collection/liste de joueurs (comportement), et vous voulez donc implémenter ICollection ou IList, ne vous gênez pas. Notionally:

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

Si vous voulez que votre objet contienne une collection/liste de joueurs (données), et que vous voulez donc que la collection ou la liste soit une propriété ou un membre, ne vous gênez pas. Notionally:

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

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

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

Quiconque essaie de vous dire que l’un de ces éléments est catégorique et définitif faux est dans tous les cas erroné. Quiconque tente de vous le dire est catégoriquement et définitivement droite dans tous les cas est mal avisé.

Passer à 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 un grand pas, et les gens associent souvent conception et mise en œuvre car il est tout à fait possible de tout parcourir dans votre tête en une seconde ou deux et de commencer à taper à l'écran.

Une expérience de pensée

Un exemple artificiel: comme d'autres l'ont mentionné, une équipe n'est pas toujours "juste" une collection de joueurs. Maintenez-vous une collection de scores de match pour l'équipe? Est-ce que l'équipe est interchangeable avec le club, dans votre modèle? Si tel est le cas, et si votre équipe est constituée d’un ensemble de joueurs, c’est peut-être aussi un ensemble d’effectifs et/ou un ensemble de scores. Ensuite, vous vous retrouvez avec:

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

Malgré la conception, à ce stade de C #, vous ne pourrez de toute façon pas implémenter tout cela en héritant de List <T>, car C # "ne supporte" que l'héritage simple. (Si vous avez essayé cette malarky en C++, vous pouvez considérer cela comme une bonne chose.) Implémenter une collection via l'héritage et l'autre via la composition est susceptible de se sentir sale. Et des propriétés telles que Count créent de la confusion chez les utilisateurs, sauf si vous implémentez explicitement ILIst <Player> .Count et IList <StaffMember> .Count, etc., puis ils sont simplement douloureux plutôt que déroutants. Vous pouvez voir où cela se passe; En vous sentant mal à l'aise en pensant à cette avenue, vous vous sentirez peut-être mal à la tête (et à tort ou à raison, vos collègues pourraient également le faire de la même manière!)

La réponse courte (trop tard)

La directive pour 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 une sagesse reçue, pas une loi. Une des raisons est que, dans la pratique, on considère que la composition l'emporte souvent sur l'héritage en termes de compréhensibilité, d'implémentabilité et de maintenabilité. Il est plus courant avec les objets du monde réel/du domaine de trouver des relations "hasa" utiles et cohérentes que des relations "isa" utiles et cohérentes, sauf si vous êtes plongé dans l'abstrait, plus particulièrement avec le temps, la précision des données et le comportement 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.

33
El Zorko

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

Si vous souhaitez utiliser LINQ, il serait plus logique de mettre en œuvre une interface générique telle que ICollection<T> ou IEnumerable<T>.

Comme mentionné, la composition est la bonne façon de s'y prendre. Il suffit de mettre en place une liste de joueurs en tant que variable privée.

31
Dmitry S.

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

Ceci 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 
}
26
Mauro Sampietro

Laissez-moi réécrire votre question. vous pouvez donc 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: "Les Aigles"

string team = new string();

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

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

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

Cela fait, vous pourrez voir s’il partage des propriétés avec d’autres classes. Et pensez à l'héritage.

24
user1852503

Ç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 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. Vous devez donc parler aux responsables de l'équipe. Dans ce contexte, l'équipe est projetée dans la liste de ses gestionnaires. Et ces deux listes ne se chevauchent généralement pas beaucoup. Les autres contextes sont les joueurs actuels par rapport aux anciens joueurs, etc.

Sémantique peu claire

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

Les classes sont extensibles

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

Les cours sont plus complexes

Dans certains cas, il peut être excessif de créer une classe supplémentaire. Chaque définition de classe doit être chargée via le chargeur de classes 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, vous pouvez peut-être considérer une équipe de football comme une liste de joueurs. Mais dans ce cas, vous devriez vraiment utiliser juste 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, dans une méthode, il est tout à fait correct d'écrire

IList<Player> footballTeam = ...

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

type FootballTeam = IList<Player>

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

22
stefan.schwetschke

Ce n’est pas parce que je pense que l’autre réponse répond à peu près à la tangente de savoir si une équipe de football "est-un" List<FootballPlayer> ou "a-a" List<FootballPlayer>, ce qui ne répond vraiment pas à cette question écrit.

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

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

Parce que List<T> n'a pas de méthodes virtuelles. Cela pose moins de problème dans votre propre code, car vous pouvez généralement basculer vers une implémentation avec relativement peu de peine - 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 code de cadre. Et rappelez-vous que les instructions référencées sont les "Instructions de conception .NET Framework" et non les "Instructions de conception .NET Application" ". Il y a une différence et, d'une manière générale, la conception des API publiques est beaucoup plus stricte.

Si mon projet actuel ne dispose pas et ne sera probablement jamais doté de cette API publique, puis-je ignorer en toute sécurité cette recommandation? 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?

Assez bien, oui. Vous voudrez peut-être examiner le raisonnement derrière cela pour voir si cela s'applique quand même à votre situation, mais si vous ne construisez pas d'API publique, vous n'avez pas particulièrement besoin de vous inquiéter des problèmes liés à l'API, tels que la gestion des versions. sous-ensemble).

Si vous ajoutez une API publique à l'avenir, vous devrez soit extraire votre API de votre implémentation (en n'exposant pas directement votre List<T>), soit enfreindre les règles avec le possible problème futur qui en découlera.

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

Dépend du contexte, mais puisque nous utilisons FootballTeam à titre d'exemple, imaginez que vous ne pouvez pas ajouter de FootballPlayer si cela pouvait amener l'équipe à dépasser le plafond salarial. 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 Add car ce n'est pas virtual (pour des raisons de performances).

Si vous vous trouvez dans une application (ce qui signifie que vous et tous vos appelants êtes compilés ensemble), vous pouvez maintenant utiliser 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 avez publiquement exposé à une tierce partie, vous venez d'effectuer un changement radical qui entraînera des erreurs de compilation et/ou d'exécution.

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

18
Mark Brackett

Est-ce que permettre aux gens de dire

myTeam.subList(3, 5);

avoir un sens du tout? Sinon, cela ne devrait pas être une liste.

16
Cruncher

Cela dépend du comportement de votre objet "team". Si elle se comporte comme une collection, il serait peut-être correct de la représenter d'abord avec une simple liste. Ensuite, vous remarquerez peut-être que vous continuez à dupliquer le code qui itère sur la liste; À ce stade, vous avez la possibilité de créer un objet FootballTeam qui enveloppe la liste des joueurs. La classe FootballTeam devient la maison pour tout le code qui itère sur la liste des joueurs.

Cela rend mon code inutilement prolixe. 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 je reçois 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, cela peut être implémenté en consultant la liste des joueurs dans une base de données. Ils n'ont pas besoin de savoir, ce qui améliore votre conception.

Cela n'a tout simplement aucun sens. Une équipe de football ne dispose pas d'une 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 de 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.

15
xpmatteo

Il y a beaucoup d’excellentes réponses ici, mais je voudrais aborder un aspect que je n’ai pas vu mentionné: La conception orientée objet concerne environ des objets habilitants .

Vous souhaitez encapsuler toutes vos règles, travail supplémentaire et détails internes dans un objet approprié. De cette façon, les autres objets qui interagissent avec celui-ci n'ont pas à s'inquiéter 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 sous forme de 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 souhaitiez faire la différence lorsqu'un joueur quitte en sachant s'il s'est retiré, s'il a démissionné ou s'il a été renvoyé. Vous pouvez implémenter une méthode RemovePlayer qui prend une énumération d'entrée appropriée. Cependant, en héritant de List, vous ne pourrez pas empêcher un accès direct à Remove, RemoveAll et même Clear. En conséquence, vous avez en fait désemparé votre FootballTeam classe.


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

Cela rend mon code inutilement prolixe. 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 petit comparé au fait que vous avez exposé List Players à tout le monde afin qu’ils puissent tripoter votre équipe sans votre consentement.

Vous continuez à dire:

Cela n'a tout simplement aucun sens. Une équipe de football ne dispose pas d'une 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 point: supprimez la "liste" des mots, et il est en fait évident qu'une équipe a des joueurs.
Cependant, vous frappez le clou avec la seconde. Vous ne voulez pas que les clients appellent ateam.Players.Add(...). Vous voulez qu'ils appellent ateam.AddPlayer(...). Et votre implémentation appellerait (éventuellement entre autres) _ en interne Players.Add(...).


J'espère que vous pourrez voir à quel point l'encapsulation est importante pour l'objectif de responsabiliser vos objets. Vous voulez permettre à chaque classe de bien faire son travail sans craindre les interférences d'autres objets.

13
Disillusioned

Quelle est la manière correcte C # de représenter une structure de données ...

Rappelez-vous: "Tous les modèles sont faux, mais certains sont utiles." - George E. P. Box

Il n'y a pas de "voie correcte", seulement une utile.

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

- édité

Ma meilleure réponse serait… ça dépend. Hériter d'une liste exposerait les clients de cette classe à des méthodes susceptibles de ne pas l'être, principalement parce que FootballTeam ressemble à une entité commerciale.

- Edition 2

Je ne me souviens sincèrement pas de ce à quoi je faisais référence dans le commentaire "ne faites pas trop d'ingénierie". Bien que je pense que l'état d'esprit KISS est un bon guide, je tiens à souligner que le fait d'hériter d'une classe d'affaires de List créerait plus de problèmes qu'il n'en résoudrait, à cause de fuite d'abstraction .

Par ailleurs, j’estime qu’il est utile d’hériter de List dans un nombre limité de cas. 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.

12
ArturoTena

Cela me rappelle le "est un" contre "a un" compromis. Parfois, il 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 exister une relation "est un". Donc, si vous écrivez dans 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 vie réelle et toute fonctionnalité de Mammifère devrait être pertinente pour Cheval. Mais pouvez-vous dire qu'une équipe est une liste? D'après ce que je peux dire, cela ne semble pas vraiment être une équipe "est une" liste. Donc, dans ce cas, j'aurais une liste en tant que variable membre.

10
Paul J Abernathy

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

Alors qu'est-ce qui m'empêche de dire:

team.Players.ByName("Nicolas")

Quand je le trouve meilleur que

team.ByName("Nicolas")

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

club.Players.ByName("Nicolas")

Les meilleures pratiques d'hier pourraient ne pas être celles de demain. Il n'y a aucune raison derrière la plupart des meilleures pratiques, la plupart ne sont qu'un large accord parmi la communauté. Au lieu de demander à la communauté si elle vous en voudra, demandez-vous ce qui est plus lisible et maintenable.

team.Players.ByName("Nicolas") 

ou

team.ByName("Nicolas")

Vraiment. Avez-vous des doutes? 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, il s'agit certainement d'une "meilleure pratique" venant de nulle part.

9
Nicolas Dorier

Selon les directives, 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 si besoin est. Cela vous permet de modifier votre décision et d'utiliser une structure de données différente sans affecter l'interface publique.

8
QuentinUK

Quand ils disent que 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é de faire respecter les règles de gestion. ou personnaliser ses fonctionnalités plus tard. Mais si vous utilisez cette classe héritée en interne dans votre projet (par opposition à une exposition potentielle à des milliers de vos clients/partenaires/autres équipes en tant qu'API), il se peut que cela soit OK si vous gagnez du temps et la fonctionnalité que vous souhaitez. dupliquer. L’avantage d’hériter de List<T>, c’est que vous éliminez beaucoup de code d’habillage muet qui ne sera jamais personnalisé dans un avenir prévisible. De plus, si vous voulez explicitement que votre classe ait exactement la même sémantique que List<T> pour la vie de vos API, elle peut également être OK.

Je vois souvent que beaucoup de gens font des tonnes de travail supplémentaire juste à cause de la règle FxCop ou parce que le blog de quelqu'un dit que c'est une "mauvaise" pratique. Plusieurs fois, cela transforme le code pour concevoir un motif étrange de palooza. Comme beaucoup de directives, considérez-le comme une directive indiquant que peut avoir des exceptions.

6
Shital Shah

Bien que je n’aie pas de comparaison complexe comme le font la plupart de ces réponses, je voudrais partager ma méthode de traitement de cette situation. En étendant IEnumerable<T>, vous pouvez autoriser votre classe Team à 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
{
    ...
}
4
Null511

Si les utilisateurs de votre classe ont besoin de toutes les méthodes et propriétés que la liste contient, vous devez en dériver votre classe. S'ils n'en ont pas besoin, incluez la liste et créez des wrappers pour les méthodes dont les utilisateurs de la classe ont réellement besoin.

Ceci est une règle stricte, si vous écrivez un API publique, ou tout autre code qui sera utilisé par beaucoup de gens. 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 choisir un autre langage moins strict. Ruby, JavaScript - tout ce qui vous permet d'écrire moins de code.

3
Ivan Nikitin

Je voulais juste ajouter que Bertrand Meyer, l'inventeur d'Eiffel et du design par contrat, aurait hérité de Team de List<Player> sans avoir à se battre.

Dans son livre, Construction de logiciel orienté objet, il décrit l’implémentation d’un système d’interface graphique où les fenêtres rectangulaires peuvent avoir des fenêtres enfants. Il a simplement Window hériter de Rectangle et Tree<Window> pour réutiliser l'implémentation.

Cependant, C # n'est pas Eiffel. Ce dernier supporte l'héritage multiple et renommer des fonctionnalités. En C #, lorsque vous sous-classez, 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 afin de renommer Add et Remove en Hire et Fire dans votre Team. Si une instance de Team est retransmise en amont vers List<Player>, l'appelant utilisera Add et Remove pour la modifier, mais vos méthodes virtuelles Hire et Fire sera appelé.

3
Alexey

Je pense que je ne suis pas d'accord avec votre généralisation. Une équipe n'est pas simplement une collection de joueurs. Une équipe a beaucoup plus d'informations à ce sujet - nom, emblème, collection de personnel de gestion/d'administration, collection de l'équipe d'entraîneurs, puis collection de joueurs. Si bien que votre classe FootballTeam devrait avoir 3 collections et ne pas être une collection; s'il s'agit de modéliser correctement le monde réel.

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

Peut-être que la notion de PlayerCollection améliore votre approche préférée?

public class PlayerCollection : Collection<Player> 
{ 
}

Et puis l'équipe de football 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();
}
0
Eniola

Préfère les interfaces aux classes

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

L'héritage rompt l'encapsulation

Dériver des classes encapsulation des pauses :

  • expose des détails internes sur la mise en œuvre de votre collection
  • déclare une interface (ensemble de fonctions et propriétés publiques) qui peut ne pas être appropriée

Entre autres choses, cela rend plus difficile la refactorisation 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é des autres parties de votre code. En bref, un _System.List_ est une implémentation spécifique d'un type de données abstrait, qui peut ou non être approprié maintenant et dans le futur.

Conceptuellement, le fait que le type de données System.List s'appelle "list" est un peu un red-hareng. A _System.List<T>_ est une collection ordonnée mutable qui prend en charge les opérations amorties O(1) pour l'ajout, l'insertion et la suppression d'éléments et les opérations O(1) pour l'extraction du numéro. d'éléments ou obtenir et définir é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. Il suffit de regarder à quel point LINQ est puissant pour une démonstration de cela.

Comment choisir les interfaces

Quand vous pensez "liste", commencez par vous dire: "Je dois représenter une collection de joueurs de baseball". Alors disons que vous décidez de modéliser cela avec une classe. Ce que vous devez faire en premier est de décider du nombre minimal d’interfaces que cette classe devra exposer.

Quelques questions qui peuvent aider à guider ce processus:

  • Dois-je avoir le compte? Si ce n'est pas le cas, implémentez IEnumerable<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érons ICollection<T>
  • L'ordre dans lequel j'ajoute des éléments à la collection est-il important? Peut-être que c'est un ISet<T> ?
  • Si vous voulez vraiment ces choses, alors allez-y et implémentez IList<T> .

De cette façon, vous ne couplerez pas d'autres parties du code avec les 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.

Remarques sur l'évitement de la plaque chauffante

L'implémentation d'interfaces dans un IDE moderne devrait être simple. Faites un clic droit et choisissez "Implémenter l'interface". Transférez ensuite toutes les implémentations vers une classe membre si nécessaire.

Cela dit, si vous vous trouvez en train d'écrire beaucoup de passe-partout, c'est probablement 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, adaptées à votre application, et peut-être juste quelques fonctions d’extension d’aide permettant de les mapper à celles dont vous avez besoin. C'est l'approche que j'ai adoptée dans ma propre interface IArray pour la bibliothèque LinqArray .

0
cdiggins

Un aspect manque. Les classes qui héritent de la liste <> ne peuvent pas être sérialisées correctement avec XmlSerializer . Dans ce cas, DataContractSerializer doit être utilisé à la place ou par une implémentation propre à la sérialisation.

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 supplémentaires: Lorsqu'une classe est héritée de List <>, XmlSerializer ne sérialise pas les autres attributs

0
marsh-wiggle