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 string
repré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 (anint
). .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 estList<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 List
est 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 List
fournie 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 Collection
classe 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 AddRange
n'est-il pas meilleur que non AddRange
?
Hériter de Collection
est 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 Collection
n'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 List
quelque chose:
class FootballTeam
{
public List<FootballPlayer> Players;
}
Il y a deux problèmes avec ceci:
Cela rend mon code inutilement verbeux. Je dois maintenant appeler
my_team.Players.Count
au lieu de justemy_team.Count
. Heureusement, avec C # je peux définir des indexeurs pour rendre l'indexation transparente, et transmettre toutes les méthodes de l'interneList
... Mais c'est beaucoup de code! Qu'est-ce que j'obtiens pour tout ce travail?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 list
de things
quelques 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?
string
est requis pour faire tout ce qu'unobject
peut faire et plus encore .Réponses:
Il y a de bonnes réponses ici. Je voudrais leur ajouter les points suivants.
Demandez à dix personnes qui ne sont pas programmeurs informatiques et qui connaissent bien le football de remplir le blanc:
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éeRoster
c'est unList<Player>
. Et faites-leReadOnlyList<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.Inacceptable pour qui? Moi? Non.
Lorsque vous créez un mécanisme qui étend le
List<T>
mécanisme .Suis-je en train de construire un mécanisme ou un objet métier ?
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.
la source
IEnumerable<T>
- une séquence ?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 concerne
List<T>
List<T>
est hautement optimisé. Son utilisation principale doit être utilisée en tant que membre privé d'un objet.class MyList<T, TX> : List<CustomObject<T, Something<TX>> { ... }
. C'est aussi simple que de le fairevar list = new MyList<int, string>();
.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.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 queSortedCollection<T>
,ObservableCollection<T>
,ReadOnlyCollection<T>
, etc. , qui mettent en œuvre chacun ,IList<T>
mais pasList<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.Si j'écrivais ce code, la classe ressemblerait probablement à ceci:
la source
List<T>
est étendu comme une classe interne privée qui utilise des génériques pour simplifier l'utilisation et la déclaration deList<T>
. Si l'extension était simplementclass 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 deList<T>
.Collection<T> allows for members (i.e. Add, Remove, etc.) to be overridden
Maintenant , c'est une bonne raison de ne pas hériter de la liste. Les autres réponses ont beaucoup trop de philosophie.Le code précédent signifie: un groupe de gars de la rue jouant au football, et ils ont un nom. Quelque chose comme:
Quoi qu'il en soit, ce code (d'après ma réponse)
Moyens: c'est une équipe de football qui a la gestion, les joueurs, les administrateurs, etc. Quelque chose comme:
Voilà comment votre logique est présentée en images…
la source
private readonly List<T> _roster = new List<T>(44);
System.Linq
espace de noms.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:
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.
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.
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.
la source
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:
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.
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.
la source
Que se passe-t-il si le pays
FootballTeam
a une équipe de réserve avec l'équipe principale?Comment modéliseriez-vous cela?
La relation est clairement a et non est un .
ou
RetiredPlayers
?En règle générale, si vous souhaitez hériter d'une collection, nommez la classe
SomethingCollection
.Votre
SomethingCollection
sémantique a- t-elle un sens? Ne faites cela que si votre type est une collection deSomething
.Dans le cas,
FootballTeam
cela ne sonne pas bien. ATeam
est plus qu'unCollection
. ATeam
peut avoir des entraîneurs, des formateurs, etc. comme l'ont indiqué les autres réponses.FootballCollection
sonne 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 deList<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
Est
X
unY
? ou aX
unY
?Mes noms de classe signifient-ils ce qu'ils sont?
la source
DefensivePlayers
,OffensivePlayers
ouOtherPlayers
, il peut être légitimement utile d'avoir un type qui pourrait être utilisé par le code qui attend unList<Player>
mais aussi inclus les membresDefensivePlayers
,OffsensivePlayers
ouSpecialPlayers
de typeIList<DefensivePlayer>
,IList<OffensivePlayer>
etIList<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 ...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:
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:
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:
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:
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.
la source
Tout d'abord, cela concerne la convivialité. Si vous utilisez l'héritage, la
Team
classe exposera les comportements (méthodes) conçus uniquement pour la manipulation d'objets. Par exemple,AsReadOnly()
ou lesCopyTo(obj)
méthodes n'ont aucun sens pour l'objet d'équipe. Au lieu de laAddRange(items)
méthode, vous voudriez probablement uneAddPlayers(players)
méthode plus descriptive .Si vous souhaitez utiliser LINQ, implémenter une interface générique telle que
ICollection<T>
ouIEnumerable<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.
la source
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:
et c'est correct:
la source
ç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:
Lorsque vous utilisez F #, il peut même être OK de créer une abréviation de type:
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.
la source
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"
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.
la source
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>
: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.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.
À 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.Cela dépend du contexte, mais puisque nous l'utilisons
FootballTeam
comme exemple - imaginez que vous ne pouvez pas ajouter unFootballPlayer
si cela ferait dépasser le plafond salarial par l'équipe. Une façon possible d'ajouter cela serait quelque chose comme:Ah ... mais vous ne pouvez pas
override Add
parce que ce n'est pas le casvirtual
(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: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.
la source
Est-ce que permettre aux gens de dire
tout sens? Sinon, ce ne devrait pas être une liste.
la source
myTeam.subTeam(3, 5);
subList
ne reviendra mêmeTeam
plus.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.
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.
Exactement :) vous direz footballTeam.Add (john), pas footballTeam.List.Add (john). La liste interne ne sera pas visible.
la source
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
RemovePlayer
méthode qui prend une énumération d'entrée appropriée. Cependant, en héritant deList
, vous ne pourrez pas empêcher l'accès direct àRemove
,RemoveAll
et mêmeClear
. En conséquence, vous avez en fait mis hors de pouvoir votreFootballTeam
classe.Réflexions supplémentaires sur l'encapsulation ... Vous avez soulevé la préoccupation suivante:
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:
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 appellentateam.AddPlayer(...)
. Et votre implémentation s'appellerait (peut-être entre autres) enPlayers.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.
la source
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.
la source
List
exposerait 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'List
un 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éthodesList
, alors ne les exposez pas.List
parce 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".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.
la source
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:
Quand je le trouve mieux que
De plus, ma PlayerCollection peut être utilisée par une autre classe, comme "Club" sans aucune duplication de code.
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?
ou
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.
la source
Collection<T>
n'est pas le même queList<T>
cela peut donc nécessiter un peu de travail pour vérifier dans un projet de grande taille.team.ByName("Nicolas")
signifie"Nicolas"
est le nom de l'équipe.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.
la source
class FootballTeam : List<FootballPlayers>
, les utilisateurs de ma classe pourront dire que je '' ai hérité deList<T>
en utilisant la réflexion, voir lesList<T>
méthodes qui ne font pas de sens pour une équipe de football, et d' être en mesure d'utiliserFootballTeam
dansList<T>
, donc j'avoir des détails révélateurs de mise en œuvre au client (inutilement).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 exposezList<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.
la source
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 votreTeam
classe à prendre en charge les extensions de requête Linq, sans exposer publiquement toutes les méthodes et propriétés deList<T>
.la source
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.
la source
Je voulais juste ajouter que Bertrand Meyer, l'inventeur d'Eiffel et du design par contrat, aurait
Team
hérité deList<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
Window
hérité des deuxRectangle
etTree<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
Add
etRemove
versHire
etFire
dans votreTeam
. Si une instance deTeam
est retransmise àList<Player>
, l'appelant l'utiliseraAdd
et laRemove
modifiera, mais vos méthodes virtuellesHire
etFire
seront appelées.la source
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?
Et puis la FootballTeam peut ressembler à ceci:
la source
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 :
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.List
est 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.List
type de données soit appelé "liste" est un peu déroutant. ASystem.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:
IEnumerable<T>
IReadonlyList<T>
.ICollection<T>
ISet<T>
?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
IArray
interface pour la bibliothèque LinqArray .la source
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.
Lectures complémentaires: Lorsqu'une classe est héritée de List <>, XmlSerializer ne sérialise pas d'autres attributs
la source
Pour citer Eric Lippert:
Par exemple, vous en avez assez de l'absence de la
AddRange
méthode dansIList<T>
:la source