Warum nicht von List <T> erben?

1399

Bei der Planung meiner Programme beginne ich oft mit einer Gedankenkette wie folgt:

Eine Fußballmannschaft ist nur eine Liste von Fußballspielern. Deshalb sollte ich es vertreten mit:

var football_team = new List<FootballPlayer>();

Die Reihenfolge dieser Liste entspricht der Reihenfolge, in der die Spieler im Kader aufgeführt sind.

Später wird mir jedoch klar, dass Teams neben der bloßen Liste der Spieler auch andere Eigenschaften haben, die aufgezeichnet werden müssen. Zum Beispiel die laufende Punktzahl in dieser Saison, das aktuelle Budget, die einheitlichen Farben, die stringDarstellung des Teamnamens usw.

Also dann denke ich:

Okay, eine Fußballmannschaft ist wie eine Liste von Spielern, aber zusätzlich hat sie einen Namen (a string) und eine laufende Summe von Punkten (an int). .NET bietet keine Klasse zum Speichern von Fußballmannschaften an, daher werde ich meine eigene Klasse erstellen. Die ähnlichste und relevanteste existierende Struktur ist List<FootballPlayer>, also werde ich davon erben:

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

Es stellt sich jedoch heraus, dass eine Richtlinie besagtList<T> , dass Sie nicht erben sollten . Diese Richtlinie verwirrt mich in zweierlei Hinsicht.

Warum nicht?

Anscheinend Listist irgendwie für die Leistung optimiert . Wie das? Welche Leistungsprobleme werde ich verursachen, wenn ich erweitere List? Was genau wird brechen?

Ein weiterer Grund, den ich gesehen habe, ist, dass er Listvon Microsoft bereitgestellt wird und ich keine Kontrolle darüber habe. Daher kann ich ihn später nicht mehr ändern, nachdem ich eine "öffentliche API" verfügbar gemacht habe . Aber ich habe Mühe, das zu verstehen. Was ist eine öffentliche API und warum sollte es mich interessieren? Kann ich diese Richtlinie ignorieren, wenn mein aktuelles Projekt diese öffentliche API nicht hat und wahrscheinlich nie haben wird? Welche Schwierigkeiten habe ich, wenn ich von erbe List und es sich herausstellt, dass ich eine öffentliche API benötige?

Warum ist das überhaupt wichtig? Eine Liste ist eine Liste. Was könnte sich möglicherweise ändern? Was könnte ich möglicherweise ändern wollen?

Und wenn Microsoft nicht wollte, dass ich erbe List, warum haben sie dann nicht die Klasse gemacht sealed?

Was soll ich sonst noch verwenden?

Anscheinend hat Microsoft für benutzerdefinierte Sammlungen eine CollectionKlasse bereitgestellt , die anstelle von erweitert werden sollte List. Aber diese Klasse ist sehr kahl und hat nicht viele nützliche Dinge, wieAddRange zum Beispiel. Die Antwort von jvitor83 liefert eine Leistungsgrundlage für diese bestimmte Methode, aber wie ist eine langsame AddRangenicht besser als nein AddRange?

Das Erben von Collectionist viel mehr Arbeit als das Erben von List, und ich sehe keinen Nutzen. Sicherlich würde Microsoft mir nicht ohne Grund sagen, dass ich zusätzliche Arbeit leisten soll, daher kann ich nicht anders, als das Gefühl zu haben, etwas falsch zu verstehen, und das Erben Collectionist eigentlich nicht die richtige Lösung für mein Problem.

Ich habe Vorschläge wie die Implementierung gesehen IList. Einfach nein. Dies sind Dutzende von Zeilen Boilerplate-Code, die mir nichts bringen.

Schließlich schlagen einige vor, das Listin etwas einzuwickeln:

class FootballTeam 
{ 
    public List<FootballPlayer> Players; 
}

Hierbei gibt es zwei Probleme:

  1. Es macht meinen Code unnötig ausführlich. Ich muss jetzt my_team.Players.Countstatt nur anrufen my_team.Count. Zum Glück kann ich mit C # Indexer definieren, um die Indizierung transparent zu machen, und alle Methoden des internen weiterleiten List... Aber das ist viel Code! Was bekomme ich für all diese Arbeit?

  2. Es macht einfach keinen Sinn. Eine Fußballmannschaft "hat" keine Liste von Spielern. Es ist die Liste der Spieler. Sie sagen nicht "John McFootballer hat sich SomeTeams Spielern angeschlossen". Sie sagen "John ist SomeTeam beigetreten". Sie fügen "Zeichen einer Zeichenfolge" keinen Buchstaben hinzu, sondern fügen einer Zeichenfolge einen Buchstaben hinzu. Sie fügen den Büchern einer Bibliothek kein Buch hinzu, sondern fügen einer Bibliothek ein Buch hinzu.

Mir ist klar, dass das, was "unter der Haube" passiert, "X zu Ys interner Liste hinzufügen" kann, aber dies scheint eine sehr kontraintuitive Art zu sein, über die Welt zu denken.

Meine Frage (zusammengefasst)

Was ist die richtige Art und Weise C # eine Datenstruktur darstellt, die „logisch“ (das heißt, „für den menschlichen Geist“) nur ist listder thingsmit einem paar Schnickschnack?

Ist das Erben von List<T>immer inakzeptabel? Wann ist es akzeptabel? Warum Warum nicht? Was muss ein Programmierer beachten, wenn er entscheidet, ob er erbt List<T>oder nicht?

Superbest
quelle
90
Ist Ihre Frage also wirklich, wann die Vererbung verwendet werden soll (ein Quadrat ist ein Rechteck, da es immer sinnvoll ist, ein Quadrat bereitzustellen, wenn ein generisches Rechteck angefordert wurde) und wann die Komposition verwendet werden soll (ein FootballTeam verfügt zusätzlich über eine Liste von FootballPlayern) zu anderen Eigenschaften, die genauso "grundlegend" sind, wie ein Name)? Würden andere Programmierer verwirrt sein, wenn Sie ihnen an anderer Stelle im Code ein FootballTeam übergeben würden, wenn sie eine einfache Liste erwarten würden?
Flug Odyssey
77
Was ist, wenn ich Ihnen sage, dass eine Fußballmannschaft eine Geschäftsfirma ist, die eine Liste von Spielerverträgen anstelle einer bloßen Liste von Spielern mit einigen zusätzlichen Eigenschaften besitzt?
STT LCU
75
Es ist wirklich ganz einfach. Ist eine Fußballmannschaft eine Liste von Spielern? Offensichtlich nicht, denn wie Sie sagen, gibt es andere relevante Eigenschaften. Hat eine Fußballmannschaft eine Liste von Spielern? Ja, es sollte also eine Liste enthalten. Abgesehen davon ist die Komposition der Vererbung im Allgemeinen vorzuziehen, da sie später leichter geändert werden kann. Wenn Sie Vererbung und Komposition gleichzeitig logisch finden, fahren Sie mit Komposition fort.
Tobberoth
45
@FlightOdyssey: Angenommen, Sie haben eine Methode SquashRectangle, die ein Rechteck nimmt, seine Höhe halbiert und seine Breite verdoppelt. Übergeben Sie nun ein Rechteck, das zufällig 4x4 ist, aber vom Typ Rechteck ist, und ein Quadrat, das 4x4 ist. Wie groß ist das Objekt, nachdem SquashRectangle aufgerufen wurde? Für das Rechteck ist es eindeutig 2x8. Wenn das Quadrat 2x8 ist, ist es kein Quadrat mehr. Wenn es nicht 2x8 ist, führt dieselbe Operation mit derselben Form zu einem anderen Ergebnis. Die Schlussfolgerung ist, dass ein veränderliches Quadrat keine Art Rechteck ist.
Eric Lippert
29
@ Gangnus: Deine Aussage ist einfach falsch; Eine echte Unterklasse ist erforderlich, um eine Funktionalität zu haben, die eine Obermenge der Oberklasse darstellt. A stringist erforderlich, um alles zu tun, was man objectkann und mehr .
Eric Lippert

Antworten:

1566

Hier gibt es einige gute Antworten. Ich würde ihnen die folgenden Punkte hinzufügen.

Was ist die richtige C # -Methode zur Darstellung einer Datenstruktur, die "logisch" (dh "für den menschlichen Verstand") nur eine Liste von Dingen mit ein paar Schnickschnack ist?

Bitten Sie zehn Nicht-Computerprogrammierer, die mit der Existenz des Fußballs vertraut sind, die Lücke auszufüllen:

Eine Fußballmannschaft ist eine besondere Art von _____

Hat jemand "Liste der Fußballspieler mit ein paar Schnickschnack" gesagt, oder haben alle "Sportmannschaft" oder "Verein" oder "Organisation" gesagt? Ihre Vorstellung, dass eine Fußballmannschaft eine bestimmte Art von Spielerliste ist, liegt in Ihrem menschlichen Verstand und nur in Ihrem menschlichen Verstand.

List<T>ist ein Mechanismus . Die Fußballmannschaft ist ein Geschäftsobjekt, dh ein Objekt, das ein Konzept darstellt, das im Geschäftsbereich des Programms liegt. Mischen Sie diese nicht! Eine Fußballmannschaft ist eine Art Mannschaft; Es hat eine Liste, eine Liste ist eine Liste von Spielern . Ein Kader ist keine bestimmte Art von Spielerliste . Ein Kader ist eine Liste von Spielern. Machen Sie also eine Eigenschaft namens Rostera List<Player>. Und machen Sie es, ReadOnlyList<Player>während Sie gerade dabei sind, es sei denn, Sie glauben, dass jeder, der etwas über eine Fußballmannschaft weiß, Spieler aus dem Kader löschen kann.

Ist das Erben von List<T>immer inakzeptabel?

Inakzeptabel für wen? Mir? Nein.

Wann ist es akzeptabel?

Wenn Sie einen Mechanismus erstellen, der den List<T>Mechanismus erweitert .

Was muss ein Programmierer beachten, wenn er entscheidet, ob er erbt List<T>oder nicht?

Baue ich einen Mechanismus oder ein Geschäftsobjekt ?

Aber das ist viel Code! Was bekomme ich für all diese Arbeit?

Sie haben mehr Zeit damit verbracht, Ihre Frage zu tippen, als Sie gebraucht hätten, um Weiterleitungsmethoden für die relevanten Mitglieder von List<T>fünfzig Mal zu schreiben . Sie haben eindeutig keine Angst vor Ausführlichkeit, und wir sprechen hier über eine sehr kleine Menge Code. Das sind ein paar Minuten Arbeit.

AKTUALISIEREN

Ich habe noch etwas darüber nachgedacht und es gibt einen weiteren Grund, eine Fußballmannschaft nicht als Liste von Spielern zu modellieren. Tatsächlich könnte es eine schlechte Idee sein, eine Fußballmannschaft so zu modellieren, dass sie auch eine Liste von Spielern hat. Das Problem mit einem Team als / mit einer Liste von Spielern ist, dass Sie zu einem bestimmten Zeitpunkt eine Momentaufnahme des Teams haben . Ich weiß nicht, was Ihr Geschäftsmodell für diese Klasse ist, aber wenn ich eine Klasse hätte, die eine Fußballmannschaft repräsentiert, würde ich ihr Fragen stellen wie "Wie viele Seahawks-Spieler haben zwischen 2003 und 2013 verletzungsbedingt Spiele verpasst?" oder "Welcher Denver-Spieler, der zuvor für ein anderes Team gespielt hat, hatte im Vergleich zum Vorjahr den größten Anstieg der Yards?" oder " Sind die Piggers dieses Jahr den ganzen Weg gegangen? "

Das heißt, eine Fußballmannschaft scheint mir als Sammlung historischer Fakten gut modelliert zu sein, z. B. als ein Spieler rekrutiert, verletzt, in den Ruhestand versetzt wurde usw. Offensichtlich ist die aktuelle Spielerliste eine wichtige Tatsache, die wahrscheinlich im Vordergrund stehen sollte. Mitte, aber es kann andere interessante Dinge geben, die Sie mit diesem Objekt tun möchten, die eine historischere Perspektive erfordern.

Eric Lippert
quelle
84
Obwohl die anderen Antworten sehr hilfreich waren, denke ich, dass diese meine Bedenken am direktesten ansprechen. Wenn Sie die Menge an Code übertreiben, haben Sie Recht, dass es am Ende nicht so viel Arbeit ist, aber ich bin sehr leicht verwirrt, wenn ich Code in mein Programm einbinde, ohne zu verstehen, warum ich ihn einbinde.
Superbest
128
@ Superbest: Ich bin froh, dass ich helfen konnte! Sie haben Recht, auf diese Zweifel zu hören; Wenn Sie nicht verstehen, warum Sie Code schreiben, finden Sie es entweder heraus oder schreiben Sie einen anderen Code, den Sie verstehen.
Eric Lippert
48
@Mehrdad Aber Sie scheinen die goldenen Regeln der Optimierung zu vergessen: 1) Tun Sie es nicht, 2) Tun Sie es noch nicht und 3) Tun Sie es nicht, ohne vorher Leistungsprofile zu erstellen, um zu zeigen, was optimiert werden muss.
AJMansfield
107
@Mehrdad: Ehrlich gesagt, wenn Ihre Anwendung erfordert, dass Sie sich um die Leistungsbelastung von beispielsweise virtuellen Methoden kümmern, ist keine moderne Sprache (C #, Java usw.) die richtige Sprache für Sie. Ich habe hier einen Laptop, auf dem jedes Arcade-Spiel, das ich als Kind gleichzeitig gespielt habe, simuliert werden kann . Dadurch kann ich mir hier und da keine Gedanken über ein Maß an Indirektion machen.
Eric Lippert
44
@ Superbest: Jetzt sind wir definitiv am Ende des Spektrums "Zusammensetzung statt Vererbung". Eine Rakete besteht aus Raketenteilen . Eine Rakete ist keine "besondere Art von Teileliste"! Ich würde Ihnen vorschlagen, dass Sie es weiter kürzen; warum eine Liste, wie auf eine gegenüberliegende IEnumerable<T>- eine Sequenz ?
Eric Lippert
161

Wow, dein Beitrag enthält eine ganze Reihe von Fragen und Punkten. Die meisten Argumente, die Sie von Microsoft erhalten, sind genau richtig. Beginnen wir mit allemList<T>

  • List<T> ist stark optimiert. Die Hauptverwendung besteht darin, als privates Mitglied eines Objekts verwendet zu werden.
  • Microsoft hat es nicht versiegelt, da Sie manchmal eine Klasse mit einem freundlicheren Namen erstellen möchten : class MyList<T, TX> : List<CustomObject<T, Something<TX>> { ... }. Jetzt ist es so einfach wie zu tun var list = new MyList<int, string>();.
  • CA1002: Legen Sie keine generischen Listen offen: Grundsätzlich lohnt es sich, diese App mit guten Codierungspraktiken zu entwickeln, auch wenn Sie diese App als alleiniger Entwickler verwenden möchten , damit sie Ihnen und der zweiten Natur einflößt. Sie können die Liste weiterhin als verfügbar machen, IList<T>wenn Sie einen Verbraucher benötigen, der über eine indizierte Liste verfügt. Auf diese Weise können Sie die Implementierung innerhalb einer Klasse später ändern.
  • Microsoft hat Collection<T>sehr allgemein gemacht, weil es ein generisches Konzept ist ... der Name sagt alles; Es ist nur eine Sammlung. Es gibt präzisere Versionen wie SortedCollection<T>, ObservableCollection<T>, ReadOnlyCollection<T>usw. , von denen jeder implementieren IList<T>aber nicht List<T> .
  • Collection<T>Ermöglicht das Überschreiben von Mitgliedern (z. B. Hinzufügen, Entfernen usw.), da diese virtuell sind. List<T>nicht.
  • Der letzte Teil Ihrer Frage ist genau richtig. Eine Fußballmannschaft ist mehr als nur eine Liste von Spielern, daher sollte es eine Klasse sein, die diese Liste von Spielern enthält. Denken Sie an Komposition gegen Vererbung . Eine Fußballmannschaft hat eine Liste von Spielern (eine Liste), es ist keine Liste von Spielern.

Wenn ich diesen Code schreiben würde, würde die Klasse wahrscheinlich ungefähr so ​​aussehen:

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.
}
meine
quelle
6
" Ihr Beitrag hat eine ganze Reihe von Fragen " - Nun, ich habe gesagt, ich war gründlich verwirrt. Vielen Dank, dass Sie auf die Frage von @ Readonly verlinkt haben. Ich sehe jetzt, dass ein großer Teil meiner Frage ein Sonderfall des allgemeineren Problems von Komposition vs. Vererbung ist.
Superbest
3
@Brian: Außer dass Sie kein Generikum mit Alias ​​erstellen können, müssen alle Typen konkret sein. In meinem Beispiel wird List<T>es als private innere Klasse erweitert, die Generika verwendet, um die Verwendung und Deklaration von zu vereinfachen List<T>. Wenn die Erweiterung nur wäre class FootballRoster : List<FootballPlayer> { }, hätte ich stattdessen einen Alias ​​verwenden können. Ich denke, es ist erwähnenswert, dass Sie zusätzliche Mitglieder / Funktionen hinzufügen können, aber es ist begrenzt, da Sie wichtige Mitglieder von nicht überschreiben können List<T>.
Mein
6
Wenn dies Programmers.SE wäre, würde ich dem aktuellen Bereich der Antwortstimmen zustimmen, aber da es sich um einen SO-Beitrag handelt, versucht diese Antwort zumindest, die C # (.NET) -spezifischen Probleme zu beantworten, obwohl es bestimmte allgemeine gibt Designprobleme.
Mark Hurd
7
Collection<T> allows for members (i.e. Add, Remove, etc.) to be overriddenNun , dass ist ein guter Grund , nicht zu vererben Liste. Die anderen Antworten haben viel zu viel Philosophie.
Navin
10
@my: Ich vermute, Sie meinen "eine Menge Fragen", da ein Sleuth ein Detektiv ist.
verrückt
152
class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal;
}

Vorheriger Code bedeutet: Ein paar Leute von der Straße, die Fußball spielen, und sie haben zufällig einen Namen. Etwas wie:

Leute, die Fußball spielen

Wie auch immer, dieser Code (aus meiner Antwort)

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

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

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

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

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

Mittel: Dies ist eine Fußballmannschaft mit Management, Spielern, Admins usw. So etwas wie:

Bild zeigt Mitglieder (und andere Attribute) des Manchester United-Teams

So wird Ihre Logik in Bildern dargestellt…

Nean Der Thal
quelle
9
Ich würde erwarten, dass Ihr zweites Beispiel ein Fußballverein ist, keine Fußballmannschaft. Ein Fußballverein hat Manager und Administratoren usw. Aus dieser Quelle : "Eine Fußballmannschaft ist der Sammelbegriff für eine Gruppe von Spielern ... Solche Mannschaften könnten ausgewählt werden, um in einem Spiel gegen eine gegnerische Mannschaft zu spielen und einen Fußballverein zu vertreten ... ". So ist es meiner Meinung nach, dass ein Team ist eine Liste der Spieler (oder vielleicht genauer eine Sammlung von Spielern).
Ben
3
Mehr optimiertprivate readonly List<T> _roster = new List<T>(44);
SiD
2
Es ist notwendig, einen System.LinqNamespace zu importieren .
Davide Cannizzo
1
Für Visuals: +1
V.7
115

Dies ist ein klassisches Beispiel für Komposition vs. Vererbung .

In diesem speziellen Fall:

Ist das Team eine Liste von Spielern mit zusätzlichem Verhalten?

oder

Ist das Team ein eigenes Objekt, das zufällig eine Liste von Spielern enthält?

Durch die Erweiterung von List beschränken Sie sich auf verschiedene Weise:

  1. Sie können den Zugriff nicht einschränken (z. B. verhindern, dass Personen den Dienstplan ändern). Sie erhalten alle List-Methoden, unabhängig davon, ob Sie sie alle benötigen / möchten oder nicht.

  2. Was passiert, wenn Sie auch Listen anderer Dinge haben möchten? Zum Beispiel haben Teams Trainer, Manager, Fans, Ausrüstung usw. Einige davon sind möglicherweise eigenständige Listen.

  3. Sie beschränken Ihre Vererbungsoptionen. Beispielsweise möchten Sie möglicherweise ein generisches Teamobjekt erstellen und dann BaseballTeam, FootballTeam usw. verwenden, die davon erben. Um von List zu erben, müssen Sie die Vererbung von Team durchführen. Dies bedeutet jedoch, dass alle verschiedenen Teamtypen gezwungen sind, dieselbe Implementierung dieses Dienstplans durchzuführen.

Zusammensetzung - einschließlich eines Objekts, das das gewünschte Verhalten in Ihrem Objekt angibt.

Vererbung - Ihr Objekt wird zu einer Instanz des Objekts mit dem gewünschten Verhalten.

Beide haben ihre Verwendung, aber dies ist ein klarer Fall, in dem die Zusammensetzung vorzuziehen ist.

Tim B.
quelle
1
Wenn Sie # 2 erweitern, ist es für eine Liste nicht sinnvoll, eine Liste zu haben .
Cruncher
Erstens hat die Frage nichts mit Komposition oder Vererbung zu tun. Die Antwort lautet: OP möchte keine spezifischere Art von Liste implementieren, daher sollte er List <> nicht erweitern. Ich bin verdreht, weil Sie sehr hohe Punktzahlen beim Stackoverflow haben und dies klar wissen sollten und die Leute vertrauen, was Sie sagen. Inzwischen glauben mindestens 55 Leute, die positiv gestimmt haben und deren Idee verwirrt ist, dass entweder die eine oder die andere Art und Weise in Ordnung ist ein System, aber es ist eindeutig nicht! # no-rage
Mauro Sampietro
6
@sam Er will das Verhalten der Liste. Er hat zwei Möglichkeiten: Er kann die Liste erweitern (Vererbung) oder eine Liste in sein Objekt aufnehmen (Komposition). Vielleicht haben Sie einen Teil der Frage oder der Antwort falsch verstanden, anstatt dass 55 Personen falsch liegen und Sie Recht haben? :)
Tim B
4
Ja. Es ist ein entarteter Fall mit nur einem Super und Sub, aber ich gebe die allgemeinere Erklärung für seine spezifische Frage. Er kann nur ein Team haben und das kann immer eine Liste haben, aber die Wahl ist immer noch, die Liste zu erben oder sie als internes Objekt einzuschließen. Sobald Sie anfangen, mehrere Arten von Teams (Fußball, Cricket usw.) einzubeziehen, und diese mehr als nur eine Liste von Spielern sind, sehen Sie, wie Sie die vollständige Frage zwischen Zusammensetzung und Erbschaft haben. In solchen Fällen ist es wichtig, das Gesamtbild zu betrachten, um frühzeitig falsche Entscheidungen zu vermeiden, die später viel Umgestaltung bedeuten.
Tim B
3
Das OP hat nicht nach Komposition gefragt, aber der Anwendungsfall ist ein klares Beispiel für ein X: Y-Problem, bei dem eines der 4 objektorientierten Programmierprinzipien gebrochen, missbraucht oder nicht vollständig verstanden wird. Die klarere Antwort besteht darin, entweder eine spezielle Sammlung wie einen Stapel oder eine Warteschlange zu schreiben (die nicht zum Anwendungsfall zu passen scheint) oder die Zusammensetzung zu verstehen. Eine Fußballmannschaft ist KEINE Liste von Fußballspielern. Ob das OP explizit danach gefragt hat oder nicht, ist irrelevant, ohne Abstraktion, Verkapselung, Vererbung und Polymorphismus zu verstehen. Sie werden die Antwort nicht verstehen, ergo, X: Y amok
Julia McGuigan
67

Wie jeder betont hat, ist ein Team von Spielern keine Liste von Spielern. Dieser Fehler wird von vielen Menschen überall gemacht, vielleicht auf verschiedenen Ebenen des Fachwissens. Oft ist das Problem subtil und gelegentlich sehr grob, wie in diesem Fall. Solche Designs sind schlecht, weil sie gegen das Liskov-Substitutionsprinzip verstoßen . Das Internet hat viele gute Artikel, die dieses Konzept erklären, z. B. http://en.wikipedia.org/wiki/Liskov_substitution_principle

Zusammenfassend gibt es zwei Regeln, die in einer Eltern / Kind-Beziehung zwischen Klassen beibehalten werden müssen:

  • Ein Kind sollte kein Merkmal benötigen, das geringer ist als das, was das Elternteil vollständig definiert.
  • Ein Elternteil sollte zusätzlich zu dem, was das Kind vollständig definiert, keine Eigenschaft benötigen.

Mit anderen Worten, ein Elternteil ist eine notwendige Definition eines Kindes, und ein Kind ist eine ausreichende Definition eines Elternteils.

Hier ist eine Möglichkeit, die eigene Lösung zu überdenken und das oben genannte Prinzip anzuwenden, um einen solchen Fehler zu vermeiden. Man sollte seine Hypothese testen, indem man überprüft, ob alle Operationen einer übergeordneten Klasse sowohl strukturell als auch semantisch für die abgeleitete Klasse gültig sind.

  • Ist eine Fußballmannschaft eine Liste von Fußballspielern? (Treffen alle Eigenschaften einer Liste auf ein Team mit derselben Bedeutung zu?)
    • Ist ein Team eine Sammlung homogener Einheiten? Ja, Team ist eine Sammlung von Spielern
    • Beschreibt die Reihenfolge der Einbeziehung der Spieler den Status des Teams und stellt das Team sicher, dass die Reihenfolge beibehalten wird, sofern dies nicht ausdrücklich geändert wird? Nein und nein
    • Wird erwartet, dass Spieler aufgrund ihrer sequenziellen Position im Team aufgenommen / fallen gelassen werden? Nein

Wie Sie sehen, gilt nur das erste Merkmal einer Liste für ein Team. Ein Team ist also keine Liste. Eine Liste ist ein Implementierungsdetail für die Verwaltung Ihres Teams. Sie sollte daher nur zum Speichern der Spielerobjekte und zum Bearbeiten mit Methoden der Teamklasse verwendet werden.

An dieser Stelle möchte ich darauf hinweisen, dass eine Teamklasse meiner Meinung nach nicht einmal mithilfe einer Liste implementiert werden sollte. In den meisten Fällen sollte es mit einer Set-Datenstruktur (z. B. HashSet) implementiert werden.

Satyan Raina
quelle
8
Netter Fang auf der Liste gegen Set. Das scheint ein nur zu häufig gemachter Fehler zu sein. Wenn eine Sammlung nur eine Instanz eines Elements enthalten kann, sollte eine Art Satz oder ein Wörterbuch ein bevorzugter Kandidat für die Implementierung sein. Es ist in Ordnung, ein Team mit zwei Spielern mit demselben Namen zu haben. Es ist nicht in Ordnung, wenn ein Spieler zweimal gleichzeitig dabei ist.
Fred Mitchell
1
+1 für einige gute Punkte, obwohl es wichtiger ist zu fragen, ob eine Datenstruktur alles Nützliche (idealerweise kurz- und langfristig) angemessen unterstützen kann, als zu tun, dass die Datenstruktur mehr tut als nützlich ist.
Tony Delroy
1
@ TonyD Nun, die Punkte, die ich anspreche, sind nicht, dass man überprüfen sollte, "ob die Datenstruktur mehr als das Nützliche tut". Es ist zu überprüfen, ob die übergeordnete Datenstruktur etwas tut, das für das Verhalten, das die untergeordnete Klasse möglicherweise impliziert, irrelevant, bedeutungslos oder kontraintuitiv ist.
Satyan Raina
1
@TonyD Es gibt tatsächlich ein Problem mit irrelevanten Merkmalen, die von einem Elternteil abgeleitet wurden, da dies in vielen Fällen die negativen Tests nicht bestehen würde. Ein Programmierer könnte Human {eat () erweitern; Lauf(); schreibe ();} von einem Gorilla {eat (); Lauf(); Swing ();} Ich denke, es ist nichts Falsches an einem Menschen mit der zusätzlichen Eigenschaft, schwingen zu können. Und dann beginnt Ihr Mensch in einer Spielwelt plötzlich, alle vermeintlichen Landhürden zu umgehen, indem er einfach über die Bäume schwingt. Sofern nicht ausdrücklich angegeben, sollte ein praktischer Mensch nicht schwingen können. Solch ein Design lässt die API sehr offen für Missbrauch und Verwirrung
Satyan Raina
1
@TonyD Ich schlage nicht vor, dass die Player-Klasse auch von HashSet abgeleitet wird. Ich schlage vor, dass die Player-Klasse "in den meisten Fällen" mit einem HashSet über Composition implementiert werden sollte. Dies ist ein Detail auf Implementierungsebene und kein Design-Level (aus diesem Grund habe ich es als Nebenbemerkung zu meiner Antwort erwähnt). Es könnte sehr gut unter Verwendung einer Liste implementiert werden, wenn es eine gültige Begründung für eine solche Implementierung gibt. Um Ihre Frage zu beantworten: Ist es notwendig, O (1) nach Schlüssel zu suchen? Nein. Daher sollte man einen Player NICHT auch von einem HashSet aus erweitern.
Satyan Raina
50

Was ist, wenn das FootballTeamReserveteam zusammen mit dem Hauptteam hat?

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

Wie würden Sie das modellieren?

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

Die Beziehung ist eindeutig hat ein und nicht ist ein .

oder RetiredPlayers?

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

Als Faustregel gilt: Wenn Sie jemals von einer Sammlung erben möchten, benennen Sie die Klasse SomethingCollection .

Tut dein SomethingCollection semantisch sinnvoll? Tun Sie dies nur, wenn Ihr Typ eine Sammlung von istSomething .

Im Falle des FootballTeam klingt es nicht richtig. A Teamist mehr als a Collection. EINTeam kann Trainer, Trainer usw. haben, wie die anderen Antworten gezeigt haben.

FootballCollection klingt wie eine Sammlung von Fußbällen oder vielleicht eine Sammlung von Fußballutensilien. TeamCollection, eine Sammlung von Teams.

FootballPlayerCollection klingt wie eine Sammlung von Spielern, die ein gültiger Name für eine Klasse wäre, von der erbt List<FootballPlayer> wenn Sie das wirklich wollten.

Ja wirklich List<FootballPlayer> ist ein perfekter Typ, um damit umzugehen. Könnte seinIList<FootballPlayer> wenn Sie es von einer Methode zurückgeben.

Zusammenfassend

Frag dich selbst

  1. Ist X ein Y? oder hat X ein Y?

  2. Bedeuten meine Klassennamen, was sie sind?

Sam Leach
quelle
Wenn der Typ eines jeden Spielers wäre es zu klassifizieren als zu entweder DefensivePlayers, OffensivePlayersoder OtherPlayerses legitim nützlich sein könnte , eine Art zu haben , die von Code verwendet werden könnten , die eine erwartet , List<Player>sondern auch enthalten Mitglieder DefensivePlayers, OffsensivePlayersoder SpecialPlayersder Typ IList<DefensivePlayer>, IList<OffensivePlayer>und IList<Player>. Man könnte ein separates Objekt verwenden, um die separaten Listen zwischenzuspeichern, aber es würde sauberer erscheinen, sie in demselben Objekt wie die Hauptliste zu kapseln [verwenden Sie die Ungültigmachung eines Listenaufzählers ...
Hauptliste
... als Hinweis darauf, dass sich die Hauptliste geändert hat und die Unterlisten beim nächsten Zugriff neu generiert werden müssen].
Supercat
2
Obwohl ich dem Punkt zustimme, zerreißt es meine Seele wirklich, wenn jemand Design-Ratschläge gibt und vorschlägt, eine konkrete Liste <T> mit einem öffentlichen Getter und Setter in einem Geschäftsobjekt zu veröffentlichen :(
sara
38

Design> Implementierung

Welche Methoden und Eigenschaften Sie verfügbar machen, ist eine Entwurfsentscheidung. Welche Basisklasse Sie erben, ist ein Implementierungsdetail. Ich denke, es lohnt sich, einen Schritt zurück zu ersteren zu machen.

Ein Objekt ist eine Sammlung von Daten und Verhalten.

Ihre ersten Fragen sollten also lauten:

  • Welche Daten enthält dieses Objekt in dem Modell, das ich erstelle?
  • Welches Verhalten zeigt dieses Objekt in diesem Modell?
  • Wie könnte sich dies in Zukunft ändern?

Denken Sie daran, dass Vererbung eine "isa" (ist eine) Beziehung impliziert, während Komposition eine "hat eine" (hasa) Beziehung impliziert. Wählen Sie aus Ihrer Sicht die richtige für Ihre Situation aus, und berücksichtigen Sie dabei, wohin die Entwicklung Ihrer Anwendung führen kann.

Denken Sie an Schnittstellen, bevor Sie an konkrete Typen denken, da es für manche Menschen einfacher ist, ihr Gehirn auf diese Weise in den "Entwurfsmodus" zu versetzen.

Dies ist nicht etwas, was jeder auf dieser Ebene bei der täglichen Codierung bewusst tut. Aber wenn Sie über diese Art von Thema nachdenken, treten Sie in Designgewässern auf. Sich dessen bewusst zu sein, kann befreiend sein.

Design-Besonderheiten berücksichtigen

Sehen Sie sich List <T> und IList <T> in MSDN oder Visual Studio an. Sehen Sie, welche Methoden und Eigenschaften sie verfügbar machen. Sehen diese Methoden aus Ihrer Sicht alle so aus, als würde jemand einem FootballTeam etwas antun wollen?

Ist FootballTeam.Reverse () für Sie sinnvoll? Sieht FootballTeam.ConvertAll <TOutput> () wie etwas aus, das Sie wollen?

Dies ist keine Trickfrage; Die Antwort könnte wirklich "Ja" sein. Wenn Sie List <Player> oder IList <Player> implementieren / erben, bleiben Sie dabei. Wenn das für Ihr Modell ideal ist, tun Sie es.

Wenn Sie sich für Ja entscheiden, ist dies sinnvoll, und Sie möchten, dass Ihr Objekt als Sammlung / Liste von Spielern (Verhalten) behandelt werden kann, und Sie möchten daher auf jeden Fall ICollection oder IList implementieren. Nominal:

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

Wenn Sie möchten, dass Ihr Objekt eine Sammlung / Liste von Spielern (Daten) enthält und Sie daher möchten, dass die Sammlung oder Liste eine Eigenschaft oder ein Mitglied ist, tun Sie dies auf jeden Fall. Nominal:

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

Möglicherweise möchten Sie, dass die Benutzer nur die Gruppe der Spieler aufzählen können, anstatt sie zu zählen, zu ihnen hinzuzufügen oder zu entfernen. IEnumerable <Player> ist eine absolut gültige Option.

Möglicherweise haben Sie das Gefühl, dass keine dieser Schnittstellen in Ihrem Modell überhaupt nützlich ist. Dies ist weniger wahrscheinlich (IEnumerable <T> ist in vielen Situationen nützlich), aber es ist immer noch möglich.

Jeder, der versucht, Ihnen zu sagen, dass eines davon in jedem Fall kategorisch und definitiv falsch ist, ist irregeführt. Jeder, der versucht, Ihnen zu sagen, dass es in jedem Fall kategorisch und definitiv richtig ist, ist irregeführt.

Fahren Sie mit der Implementierung fort

Sobald Sie sich für Daten und Verhalten entschieden haben, können Sie eine Entscheidung über die Implementierung treffen. Dies schließt ein, von welchen konkreten Klassen Sie über Vererbung oder Zusammensetzung abhängig sind.

Dies ist möglicherweise kein großer Schritt, und die Leute verbinden häufig Design und Implementierung, da es durchaus möglich ist, in ein oder zwei Sekunden alles in Ihrem Kopf durchzugehen und mit dem Tippen zu beginnen.

Ein Gedankenexperiment

Ein künstliches Beispiel: Wie andere bereits erwähnt haben, ist ein Team nicht immer "nur" eine Ansammlung von Spielern. Führen Sie eine Sammlung von Spielergebnissen für die Mannschaft? Ist die Mannschaft in Ihrem Modell mit dem Verein austauschbar? Wenn ja, und wenn Ihr Team eine Sammlung von Spielern ist, ist es vielleicht auch eine Sammlung von Mitarbeitern und / oder eine Sammlung von Punktzahlen. Dann haben Sie am Ende:

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

Ungeachtet des Designs können Sie zu diesem Zeitpunkt in C # ohnehin nicht alle diese Funktionen implementieren, indem Sie von List <T> erben, da C # "nur" die Einzelvererbung unterstützt. (Wenn Sie diese Malarkie in C ++ ausprobiert haben, können Sie dies als eine gute Sache betrachten.) Das Implementieren einer Sammlung über Vererbung und einer über Komposition fühlt sich wahrscheinlich schmutzig an. Und Eigenschaften wie Count werden für Benutzer verwirrend, es sei denn, Sie implementieren ILIst <Player> .Count und IList <StaffMember> .Count usw. explizit, und dann sind sie eher schmerzhaft als verwirrend. Sie können sehen, wohin das führt; Das Bauchgefühl beim Nachdenken über diese Straße kann Ihnen durchaus sagen, dass es sich falsch anfühlt, in diese Richtung zu gehen (und zu Recht oder zu Unrecht könnten Ihre Kollegen dies auch tun, wenn Sie es auf diese Weise umgesetzt haben!).

Die kurze Antwort (zu spät)

Die Richtlinie, nicht von Sammlungsklassen zu erben, ist nicht C # -spezifisch. Sie finden sie in vielen Programmiersprachen. Es wird Weisheit empfangen, kein Gesetz. Ein Grund dafür ist, dass in der Praxis die Komposition in Bezug auf Verständlichkeit, Implementierbarkeit und Wartbarkeit häufig die Vererbung gewinnt. Bei Objekten aus der realen Welt / Domäne ist es üblicher, nützliche und konsistente "hasa" -Beziehungen zu finden als nützliche und konsistente "isa" -Beziehungen, es sei denn, Sie sind tief in der Zusammenfassung verwurzelt, insbesondere im Laufe der Zeit und der genauen Daten und des Verhaltens von Objekten im Code Änderungen. Dies sollte nicht dazu führen, dass Sie das Erben von Auflistungsklassen immer ausschließen. aber es kann suggestiv sein.

El Zorko
quelle
34

Zuallererst hat es mit Usability zu tun. Wenn Sie die Vererbung verwenden, macht die TeamKlasse Verhalten (Methoden) verfügbar, die ausschließlich für die Objektmanipulation ausgelegt sind. Zum Beispiel AsReadOnly()oder CopyTo(obj)Methoden machen für das Teamobjekt keinen Sinn. Anstelle der AddRange(items)Methode möchten Sie wahrscheinlich eine aussagekräftigereAddPlayers(players) Methode.

Wenn Sie LINQ verwenden möchten, implementieren Sie eine generische Schnittstelle wie ICollection<T>oderIEnumerable<T> sinnvoller.

Wie bereits erwähnt, ist Komposition der richtige Weg. Implementieren Sie einfach eine Liste von Spielern als private Variable.

Dmitry S.
quelle
30

Eine Fußballmannschaft ist keine Liste von Fußballspielern. Eine Fußballmannschaft besteht aus einer Liste von Fußballspielern!

Das ist logisch falsch:

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

und das ist richtig:

class FootballTeam 
{ 
    public List<FootballPlayer> players
    public string TeamName; 
    public int RunningTotal 
}
Mauro Sampietro
quelle
19
Das erklärt nicht warum . Der OP weiß, dass die meisten anderen Programmierer so denken, er versteht einfach nicht, warum es wichtig ist. Dies liefert nur wirklich Informationen, die bereits in der Frage enthalten sind.
Servy
2
Dies ist das erste, woran ich auch dachte, wie seine Daten nur falsch modelliert wurden. Der Grund dafür ist, dass Ihr Datenmodell falsch ist.
Ball
3
Warum nicht von der Liste erben? Weil er keine spezifischere Art von Liste will.
Mauro Sampietro
2
Ich denke nicht, dass das zweite Beispiel richtig ist. Sie legen den Zustand frei und unterbrechen damit die Kapselung. Der Status sollte im Objekt verborgen / intern sein, und die Art und Weise, diesen Status zu ändern, sollte über öffentliche Methoden erfolgen. Welche Methoden genau aus den Geschäftsanforderungen zu bestimmen sind, sollte jedoch auf der Basis "Jetzt brauchen" und nicht auf der Basis "Ich weiß nicht, ob es irgendwann nützlich sein kann" basieren. Halten Sie Ihre API fest und Sie sparen sich Zeit und Mühe.
Sara
1
Verkapselung ist nicht der Punkt der Frage. Ich konzentriere mich darauf, einen Typ nicht zu erweitern, wenn Sie nicht möchten, dass sich dieser Typ spezifischer verhält. Diese Felder sollten Autoproperties in c # und get / set-Methoden in anderen Sprachen sein, aber hier in diesem Zusammenhang ist das völlig überflüssig
Mauro Sampietro
28

Das hängt vom Kontext ab

Wenn Sie Ihre Mannschaft als eine Liste von Spielern betrachten, projizieren Sie die "Idee" einer Fußballmannschaft auf einen Aspekt: ​​Sie reduzieren die "Mannschaft" auf die Personen, die Sie auf dem Spielfeld sehen. Diese Projektion ist nur in einem bestimmten Kontext korrekt. In einem anderen Kontext könnte dies völlig falsch sein. Stellen Sie sich vor, Sie möchten Sponsor des Teams werden. Sie müssen also mit den Managern des Teams sprechen. In diesem Zusammenhang wird das Team auf die Liste seiner Manager projiziert. Und diese beiden Listen überschneiden sich normalerweise nicht sehr. Andere Kontexte sind die aktuellen gegen die früheren Spieler usw.

Unklare Semantik

Das Problem bei der Betrachtung eines Teams als Liste seiner Spieler besteht darin, dass seine Semantik vom Kontext abhängt und nicht erweitert werden kann, wenn sich der Kontext ändert. Außerdem ist es schwer auszudrücken, welchen Kontext Sie verwenden.

Klassen sind erweiterbar

Wenn Sie eine Klasse mit nur einem Mitglied verwenden (z IList activePlayers ) verwenden, können Sie den Namen des Mitglieds (und zusätzlich seinen Kommentar) verwenden, um den Kontext zu verdeutlichen. Wenn es zusätzliche Kontexte gibt, fügen Sie einfach ein zusätzliches Mitglied hinzu.

Klassen sind komplexer

In einigen Fällen kann es übertrieben sein, eine zusätzliche Klasse zu erstellen. Jede Klassendefinition muss über den Klassenladeprogramm geladen werden und wird von der virtuellen Maschine zwischengespeichert. Dies kostet Sie Laufzeitleistung und Speicher. Wenn Sie einen ganz bestimmten Kontext haben, ist es möglicherweise in Ordnung, eine Fußballmannschaft als Liste von Spielern zu betrachten. Aber in diesem Fall sollten Sie wirklich nur a verwenden IList , keine daraus abgeleitete Klasse verwenden.

Schlussfolgerung / Überlegungen

Wenn Sie einen ganz bestimmten Kontext haben, ist es in Ordnung, ein Team als Liste von Spielern zu betrachten. Zum Beispiel ist es innerhalb einer Methode völlig in Ordnung zu schreiben:

IList<Player> footballTeam = ...

Bei Verwendung von F # kann es sogar in Ordnung sein, eine Typabkürzung zu erstellen:

type FootballTeam = IList<Player>

Wenn der Kontext jedoch breiter oder sogar unklar ist, sollten Sie dies nicht tun. Dies ist insbesondere dann der Fall, wenn Sie eine neue Klasse erstellen, deren Kontext, in dem sie möglicherweise in Zukunft verwendet wird, nicht klar ist. Ein Warnzeichen ist, wenn Sie Ihrer Klasse zusätzliche Attribute hinzufügen (Name der Mannschaft, des Trainers usw.). Dies ist ein klares Zeichen dafür, dass der Kontext, in dem die Klasse verwendet wird, nicht festgelegt ist und sich in Zukunft ändern wird. In diesem Fall können Sie das Team nicht als Liste von Spielern betrachten, aber Sie sollten die Liste der (derzeit aktiven, nicht verletzten usw.) Spieler als Attribut des Teams modellieren.

stefan.schwetschke
quelle
27

Lassen Sie mich Ihre Frage umschreiben. Sie können das Thema also aus einer anderen Perspektive betrachten.

Wenn ich eine Fußballmannschaft vertreten muss, verstehe ich, dass es im Grunde ein Name ist. Wie: "Die Adler"

string team = new string();

Später wurde mir klar, dass Teams auch Spieler haben.

Warum kann ich den Stringtyp nicht einfach so erweitern, dass er auch eine Liste von Spielern enthält?

Ihr Einstieg in das Problem ist willkürlich. Versuchen Sie zu überlegen, was ein Team hat (Eigenschaften), nicht was es ist .

Nachdem Sie dies getan haben, können Sie sehen, ob es Eigenschaften mit anderen Klassen teilt. Und denken Sie an Vererbung.

user1852503
quelle
1
Das ist ein guter Punkt - man könnte sich das Team nur als einen Namen vorstellen. Wenn meine Anwendung jedoch darauf abzielt, mit den Aktionen der Spieler zu arbeiten, ist dieses Denken etwas weniger offensichtlich. Wie auch immer, es scheint, dass das Problem letztendlich auf Komposition vs. Vererbung zurückzuführen ist.
Superbest
6
Das ist eine verdienstvolle Sichtweise. Als Randnotiz beachten Sie bitte Folgendes: Ein Mann mit Reichtum besitzt mehrere Fußballmannschaften, er gibt eine an einen Freund als Geschenk, der Freund ändert den Namen der Mannschaft, entlässt den Trainer, und ersetzt alle Spieler. Das Team der Freunde trifft die Herrenmannschaft auf dem Grün und als die Herrenmannschaft verliert, sagt er: "Ich kann nicht glauben, dass du mich mit der Mannschaft schlägst, die ich dir gegeben habe!" Ist der Mann richtig? Wie würden Sie das überprüfen?
user1852503
20

Nur weil ich denke, dass die anderen Antworten ziemlich stark davon abhängen, ob eine Fußballmannschaft "ein" List<FootballPlayer>oder "ein" ist List<FootballPlayer>, was diese Frage wirklich nicht wie geschrieben beantwortet.

Das OP bittet hauptsächlich um Klarstellung der Richtlinien für das Erben von List<T>:

Eine Richtlinie besagt, dass Sie nicht von erben sollten List<T>. Warum nicht?

Da List<T>hat keine virtuellen Methoden. Dies ist in Ihrem eigenen Code weniger problematisch, da Sie die Implementierung normalerweise mit relativ geringem Aufwand austauschen können - in einer öffentlichen API kann dies jedoch ein viel größeres Problem sein.

Was ist eine öffentliche API und warum sollte es mich interessieren?

Eine öffentliche API ist eine Schnittstelle, die Sie Programmierern von Drittanbietern zur Verfügung stellen. Denken Sie an Framework-Code. Denken Sie daran, dass es sich bei den Richtlinien, auf die verwiesen wird, um die ".NET Framework- Entwurfsrichtlinien" und nicht um die ".NET Application Design-Richtlinien" handelt. Es gibt einen Unterschied, und im Allgemeinen ist das öffentliche API-Design viel strenger.

Kann ich diese Richtlinie ignorieren, wenn mein aktuelles Projekt diese öffentliche API nicht hat und wahrscheinlich nie haben wird? Welche Schwierigkeiten habe ich, wenn ich von List erbe und es sich herausstellt, dass ich eine öffentliche API benötige?

Ziemlich genau, ja. Möglicherweise möchten Sie die Gründe dafür berücksichtigen, um festzustellen, ob sie ohnehin für Ihre Situation zutreffen. Wenn Sie jedoch keine öffentliche API erstellen, müssen Sie sich nicht besonders um API-Probleme wie die Versionierung kümmern (von denen dies eine ist Teilmenge).

Wenn Sie in Zukunft eine öffentliche API hinzufügen, müssen Sie entweder Ihre API von Ihrer Implementierung abstrahieren (indem Sie Ihre nicht List<T>direkt offenlegen ) oder die Richtlinien mit den möglichen zukünftigen Schmerzen verletzen, die damit verbunden sind.

Warum ist das überhaupt wichtig? Eine Liste ist eine Liste. Was könnte sich möglicherweise ändern? Was könnte ich möglicherweise ändern wollen?

Hängt vom Kontext ab, aber da wir dies FootballTeamals Beispiel verwenden - stellen Sie sich vor, Sie können kein hinzufügen, FootballPlayerwenn dies dazu führen würde, dass das Team die Gehaltsobergrenze überschreitet. Ein möglicher Weg, dies hinzuzufügen, wäre etwa:

 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 ... aber Sie können nicht, override Addweil es nicht ist virtual(aus Leistungsgründen).

Wenn Sie sich in einer Anwendung befinden (was im Grunde bedeutet, dass Sie und alle Ihre Anrufer zusammen kompiliert werden), können Sie jetzt IList<T>alle Kompilierungsfehler verwenden und beheben:

 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 */
 }

Wenn Sie jedoch öffentlich einem Drittanbieter ausgesetzt waren, haben Sie gerade eine wichtige Änderung vorgenommen, die zu Kompilierungs- und / oder Laufzeitfehlern führt.

TL; DR - Die Richtlinien gelten für öffentliche APIs. Machen Sie für private APIs, was Sie wollen.

Mark Brackett
quelle
18

Erlaubt es den Leuten zu sagen

myTeam.subList(3, 5);

überhaupt einen Sinn ergeben? Wenn nicht, sollte es keine Liste sein.

Cruncher
quelle
3
Es könnte sein, wenn Sie es nanntenmyTeam.subTeam(3, 5);
Sam Leach
3
@SamLeach Angenommen, das stimmt, dann benötigen Sie immer noch Komposition und keine Vererbung. Da subListwird nicht mal mehr ein zurückkehren Team.
Cruncher
1
Das hat mich mehr überzeugt als jede andere Antwort. +1
FireCubez
16

Dies hängt vom Verhalten Ihres "Team" -Objekts ab. Wenn es sich wie eine Sammlung verhält, ist es möglicherweise in Ordnung, es zuerst mit einer einfachen Liste darzustellen. Dann werden Sie möglicherweise feststellen, dass Sie weiterhin Code duplizieren, der in der Liste wiederholt wird. Zu diesem Zeitpunkt haben Sie die Möglichkeit, ein FootballTeam-Objekt zu erstellen, das die Liste der Spieler umschließt. Die FootballTeam-Klasse wird zur Heimat des gesamten Codes, der auf der Liste der Spieler wiederholt wird.

Es macht meinen Code unnötig ausführlich. Ich muss jetzt my_team.Players.Count anstelle von my_team.Count aufrufen. Zum Glück kann ich mit C # Indexer definieren, um die Indizierung transparent zu machen, und alle Methoden der internen Liste weiterleiten ... Aber das ist viel Code! Was bekomme ich für all diese Arbeit?

Verkapselung. Ihre Kunden müssen nicht wissen, was in FootballTeam vor sich geht. Wie alle Ihre Kunden wissen, kann dies implementiert werden, indem Sie die Liste der Spieler in einer Datenbank nachschlagen. Sie müssen es nicht wissen, und dies verbessert Ihr Design.

Es macht einfach keinen Sinn. Eine Fußballmannschaft "hat" keine Liste von Spielern. Es ist die Liste der Spieler. Sie sagen nicht "John McFootballer hat sich SomeTeams Spielern angeschlossen". Sie sagen "John ist SomeTeam beigetreten". Sie fügen "Zeichen einer Zeichenfolge" keinen Buchstaben hinzu, sondern einer Zeichenfolge einen Buchstaben. Sie fügen den Büchern einer Bibliothek kein Buch hinzu, sondern fügen einer Bibliothek ein Buch hinzu.

Genau :) Sie werden sagen, FootballTeam.Add (John), nicht FootballTeam.List.Add (John). Die interne Liste ist nicht sichtbar.

xpmatteo
quelle
1
OP möchte verstehen, wie man REALITÄT MODELLIERT, definiert dann aber eine Fußballmannschaft als eine Liste von Fußballspielern (was konzeptionell falsch ist). Das ist das Problem. Jedes andere Argument führt ihn meiner Meinung nach in die Irre.
Mauro Sampietro
3
Ich bin nicht der Meinung, dass die Liste der Spieler konzeptionell falsch ist. Nicht unbedingt. Wie jemand anderes auf dieser Seite schrieb: "Alle Modelle sind falsch, aber einige sind nützlich"
xpmatteo
Richtige Modelle sind schwer zu bekommen, wenn sie fertig sind, sind sie diejenigen, die nützlich sind. Wenn ein Modell missbraucht werden kann, ist es konzeptionell falsch. stackoverflow.com/a/21706411/711061
Mauro Sampietro
15

Hier gibt es viele hervorragende Antworten, aber ich möchte auf etwas eingehen, das ich nicht erwähnt habe: Bei objektorientiertem Design geht es darum , Objekte zu stärken .

Sie möchten alle Ihre Regeln, zusätzlichen Arbeiten und internen Details in einem geeigneten Objekt zusammenfassen. Auf diese Weise müssen sich andere Objekte, die mit diesem interagieren, nicht um alles kümmern. Sie möchten sogar noch einen Schritt weiter gehen und aktiv verhindern, dass andere Objekte diese Interna umgehen.

Wenn Sie von erben List , können alle anderen Objekte Sie als Liste sehen. Sie haben direkten Zugriff auf die Methoden zum Hinzufügen und Entfernen von Spielern. Und du hast deine Kontrolle verloren; zum Beispiel:

Angenommen, Sie möchten unterscheiden, wann ein Spieler abreist, indem Sie wissen, ob er in den Ruhestand getreten, zurückgetreten oder entlassen wurde. Sie können eine RemovePlayerMethode implementieren , die eine entsprechende Eingabeaufzählung verwendet. Jedoch durch Erben von List, würden Sie nicht in der Lage einen direkten Zugang zu verhindern Remove, RemoveAllund sogar Clear. Infolgedessen haben Sie Ihre Klasse tatsächlich entmachtetFootballTeam .


Zusätzliche Gedanken zur Kapselung ... Sie haben folgende Bedenken geäußert:

Es macht meinen Code unnötig ausführlich. Ich muss jetzt my_team.Players.Count anstelle von my_team.Count aufrufen.

Sie haben Recht, das wäre unnötig ausführlich für alle Kunden, um Ihr Team zu nutzen. Dieses Problem ist jedoch im Vergleich zu der Tatsache, dass Sie List Playersallen und jedem ausgesetzt waren , sehr gering, sodass sie ohne Ihre Zustimmung mit Ihrem Team herumspielen können.

Sie sagen weiter:

Es macht einfach keinen Sinn. Eine Fußballmannschaft "hat" keine Liste von Spielern. Es ist die Liste der Spieler. Sie sagen nicht "John McFootballer hat sich SomeTeams Spielern angeschlossen". Sie sagen "John ist SomeTeam beigetreten".

Beim ersten Punkt liegen Sie falsch: Lassen Sie das Wort "Liste" fallen, und es ist tatsächlich offensichtlich, dass ein Team Spieler hat.
Sie treffen jedoch mit der Sekunde den Nagel auf den Kopf. Sie möchten nicht, dass Kunden anrufen ateam.Players.Add(...). Sie wollen, dass sie anrufen ateam.AddPlayer(...). Und Ihre Implementierung würde (möglicherweise unter anderem) Players.Add(...)intern aufgerufen .


Hoffentlich können Sie sehen, wie wichtig die Kapselung für das Ziel ist, Ihre Objekte zu stärken. Sie möchten, dass jede Klasse ihre Arbeit gut erledigt, ohne Angst vor Störungen durch andere Objekte zu haben.

Desillusioniert
quelle
12

Was ist die richtige C # Art, eine Datenstruktur darzustellen ...

Denken Sie daran: "Alle Modelle sind falsch, aber einige sind nützlich." - George EP Box

Es gibt keinen "richtigen Weg", nur einen nützlichen.

Wählen Sie eine, die für Sie und / oder Ihre Benutzer nützlich ist. Das ist es. Wirtschaftlich entwickeln, nicht überentwickeln. Je weniger Code Sie schreiben, desto weniger Code müssen Sie debuggen. (Lesen Sie die folgenden Ausgaben).

- Bearbeitet

Meine beste Antwort wäre ... es kommt darauf an. Das Erben von einer Liste würde die Clients dieser Klasse Methoden aussetzen, die möglicherweise nicht verfügbar gemacht werden sollten, vor allem, weil FootballTeam wie eine Geschäftseinheit aussieht.

- Ausgabe 2

Ich erinnere mich aufrichtig nicht an das, worauf ich mich in dem Kommentar „Nicht überentwickeln“ bezog. Obwohl ich der Meinung bin, dass die KISS- Denkweise ein guter Leitfaden ist, möchte ich betonen, dass das Erben einer Business-Klasse von List aufgrund von Abstraktionsverlusten mehr Probleme verursachen als lösen würde .

Andererseits glaube ich, dass es eine begrenzte Anzahl von Fällen gibt, in denen es nützlich ist, einfach von List zu erben. Wie ich in der vorherigen Ausgabe geschrieben habe, kommt es darauf an. Die Antwort auf jeden Fall wird stark von Wissen, Erfahrung und persönlichen Vorlieben beeinflusst.

Vielen Dank an @kai, der mir geholfen hat, genauer über die Antwort nachzudenken.

ArturoTena
quelle
Meine beste Antwort wäre ... es kommt darauf an. Das Erben von einer Liste würde die Clients dieser Klasse Methoden aussetzen, die möglicherweise nicht verfügbar gemacht werden sollten, vor allem, weil FootballTeam wie eine Geschäftseinheit aussieht. Ich weiß nicht, ob ich diese Antwort bearbeiten oder eine andere posten soll.
ArturoTena
2
Das Bit über " Erben von a Listwürde die Clients dieser Klasse Methoden aussetzen, die möglicherweise nicht verfügbar gemacht werden sollten " ist genau das, was das Erben von Listeinem absoluten Nein-Nein ausmacht . Sobald sie freigelegt sind, werden sie im Laufe der Zeit allmählich missbraucht und missbraucht. Zeitersparnis durch "wirtschaftliche Entwicklung" kann leicht zu einer Verzehnfachung der in Zukunft verlorenen Einsparungen führen: Debuggen der Missbräuche und eventuelles Refactoring, um die Vererbung zu beheben. Das YAGNI-Prinzip kann auch als Bedeutung angesehen werden: Sie werden nicht alle diese Methoden Listbenötigen, also legen Sie sie nicht offen.
Desillusioniert
3
"Überentwickeln Sie nicht. Je weniger Code Sie schreiben, desto weniger Code müssen Sie debuggen." <- Ich denke, das ist irreführend. Kapselung und Zusammensetzung über Vererbung sind NICHT überentwickelt und erfordern kein Debuggen mehr. Durch die Kapselung begrenzen Sie die Anzahl der Möglichkeiten, wie Clients Ihre Klasse verwenden (und missbrauchen) können, und haben daher weniger Einstiegspunkte, die getestet und validiert werden müssen. Das Erben von, Listweil es schnell und einfach ist und somit zu weniger Fehlern führen würde, ist einfach falsch. Es ist nur schlechtes Design und schlechtes Design führt zu viel mehr Fehlern als "Over Engineering".
Sara
@kai Ich stimme dir in jedem Punkt zu. Ich erinnere mich aufrichtig nicht an das, worauf ich mich in dem Kommentar „Nicht überentwickeln“ bezog. OTOH, ich glaube, es gibt eine begrenzte Anzahl von Fällen, in denen es nützlich ist, einfach von List zu erben. Wie ich in der späteren Ausgabe geschrieben habe, kommt es darauf an. Die Antwort auf jeden Fall wird stark von Wissen, Erfahrung und persönlichen Vorlieben beeinflusst. Wie alles im Leben. ;-)
ArturoTena
11

Dies erinnert mich an das "Ist ein" gegen "hat einen" Kompromiss. Manchmal ist es einfacher und sinnvoller, direkt von einer Superklasse zu erben. In anderen Fällen ist es sinnvoller, eine eigenständige Klasse zu erstellen und die Klasse, von der Sie geerbt hätten, als Mitgliedsvariable einzuschließen. Sie können weiterhin auf die Funktionalität der Klasse zugreifen, sind jedoch nicht an die Schnittstelle oder andere Einschränkungen gebunden, die möglicherweise durch das Erben von der Klasse entstehen.

Was machst du Wie bei vielen Dingen ... hängt es vom Kontext ab. Die Anleitung, die ich verwenden würde, ist, dass es wirklich eine "ist eine" Beziehung geben sollte, um von einer anderen Klasse zu erben. Wenn Sie also eine Klasse namens BMW schreiben, könnte sie von Car erben, weil ein BMW wirklich ein Auto ist. Eine Pferdeklasse kann von der Säugetierklasse erben, da ein Pferd im wirklichen Leben tatsächlich ein Säugetier ist und jede Säugetierfunktionalität für Pferd relevant sein sollte. Aber können Sie sagen, dass ein Team eine Liste ist? Soweit ich das beurteilen kann, scheint es nicht so, als ob ein Team wirklich eine Liste ist. In diesem Fall hätte ich also eine Liste als Mitgliedsvariable.

Paul J Abernathy
quelle
9

Mein schmutziges Geheimnis: Es ist mir egal, was die Leute sagen, und ich mache es. .NET Framework wird mit "XxxxCollection" (UIElementCollection für das beste Beispiel) verbreitet.

Was hält mich davon ab zu sagen:

team.Players.ByName("Nicolas")

Wenn ich es besser finde als

team.ByName("Nicolas")

Darüber hinaus kann meine PlayerCollection von anderen Klassen wie "Club" ohne Code-Duplizierung verwendet werden.

club.Players.ByName("Nicolas")

Best Practices von gestern sind möglicherweise nicht die von morgen. Es gibt keinen Grund für die meisten Best Practices, die meisten sind sich nur in der Community einig. Anstatt die Community zu fragen, ob sie Ihnen dabei die Schuld geben wird, fragen Sie sich, was ist besser lesbar und wartbar?

team.Players.ByName("Nicolas") 

oder

team.ByName("Nicolas")

Ja wirklich. Hast du irgendwelche Zweifel? Vielleicht müssen Sie jetzt mit anderen technischen Einschränkungen spielen, die Sie daran hindern, List <T> in Ihrem realen Anwendungsfall zu verwenden. Fügen Sie jedoch keine Einschränkung hinzu, die nicht vorhanden sein sollte. Wenn Microsoft das Warum nicht dokumentiert hat, dann ist es sicherlich eine "Best Practice", die aus dem Nichts kommt.

Nicolas Dorier
quelle
9
Es ist zwar wichtig, den Mut und die Initiative zu haben, um die akzeptierte Weisheit gegebenenfalls in Frage zu stellen, aber ich denke, es ist ratsam, zuerst zu verstehen, warum die akzeptierte Weisheit überhaupt akzeptiert wurde, bevor man sie in Frage stellt. -1 weil "Ignoriere diese Richtlinie!" ist keine gute Antwort auf "Warum gibt es diese Richtlinie?" Übrigens hat die Community mich in diesem Fall nicht nur "beschuldigt", sondern auch eine zufriedenstellende Erklärung geliefert, die mich überzeugen konnte.
Superbest
3
Wenn keine Erklärung abgegeben wird, besteht Ihr einziger Weg darin, die Grenze zu überschreiten und zu testen, ohne Angst vor Verstößen zu haben. Jetzt. Fragen Sie sich, auch wenn List <T> keine gute Idee war. Wie schmerzhaft wäre es, die Vererbung von Liste <T> in Sammlung <T> zu ändern? Eine Vermutung: Weniger Zeit als das Posten im Forum, unabhängig von der Länge Ihres Codes mit Refactoring-Tools. Jetzt ist es eine gute Frage, aber keine praktische.
Nicolas Dorier
Zur Verdeutlichung: Ich warte sicherlich nicht auf den Segen von SO, um von allem zu erben, was ich will, wie ich will, aber der Punkt meiner Frage war, die Überlegungen zu verstehen, die in diese Entscheidung einfließen sollten, und warum so viele erfahrene Programmierer zu haben scheinen entschied das Gegenteil von dem, was ich tat. Collection<T>ist nicht dasselbe wie List<T>so, dass es möglicherweise einige Arbeit erfordert, um in einem großen Projekt zu überprüfen.
Superbest
2
team.ByName("Nicolas")Mittel "Nicolas"ist der Name des Teams.
Sam Leach
1
"Fügen Sie keine Einschränkung hinzu, die nicht existieren sollte" Au contraire, legen Sie nichts offen, für das Sie keinen guten Grund haben. Dies ist weder Purismus noch blinder Eifer, noch ist es der neueste Design-Modetrend. Es ist grundlegendes objektorientiertes Design 101, Anfängerniveau. Es ist kein Geheimnis, warum diese "Regel" existiert, sie ist das Ergebnis jahrzehntelanger Erfahrung.
Sara
8

Die Richtlinien besagen, dass die öffentliche API nicht die interne Entwurfsentscheidung darüber offenlegen sollte, ob Sie eine Liste, einen Satz, ein Wörterbuch, einen Baum oder was auch immer verwenden. Ein "Team" ist nicht unbedingt eine Liste. Sie können es als Liste implementieren, aber Benutzer Ihrer öffentlichen API sollten Ihre Klasse nach Bedarf verwenden. Auf diese Weise können Sie Ihre Entscheidung ändern und eine andere Datenstruktur verwenden, ohne die öffentliche Schnittstelle zu beeinträchtigen.

QuentinUK
quelle
Rückblickend haben Sie nach der Erklärung von @EricLippert und anderen tatsächlich eine großartige Antwort auf den API-Teil meiner Frage gegeben - zusätzlich zu dem, was Sie gesagt haben class FootballTeam : List<FootballPlayers>, können Benutzer meiner Klasse in diesem Fall sagen, dass ich ' habe geerbt von List<T>durch Reflexion, sieht die Verwendung von List<T>Methoden, die keinen Sinn für eine Fußballmannschaft machen, und in der Lage sein zu verwenden , FootballTeamin List<T>, also würde ich an den Client aufschlussImplementierungsDetails wird (unnötig).
Superbest
7

Wenn sie sagen List<T>, dass es "optimiert" ist, wollen sie damit meinen, dass es keine Funktionen wie virtuelle Methoden gibt, die etwas teurer sind. Das Problem ist also, dass Sie, sobald Sie List<T>in Ihrer öffentlichen API verfügbar gemacht haben , nicht mehr in der Lage sind, Geschäftsregeln durchzusetzen oder deren Funktionalität später anzupassen. Wenn Sie diese geerbte Klasse jedoch als intern in Ihrem Projekt verwenden (im Gegensatz dazu, dass sie möglicherweise Tausenden Ihrer Kunden / Partner / anderen Teams als API ausgesetzt ist), ist es möglicherweise in Ordnung, wenn dies Ihre Zeit spart und die gewünschte Funktionalität bietet Duplikat. Der Vorteil des Erbens von für die Lebensdauer Ihrer APIs kann dann auch in Ordnung sein.List<T> besteht darin, dass Sie viel dummen Wrapper-Code eliminieren, der in absehbarer Zukunft niemals angepasst werden wird. Auch wenn Sie möchten, dass Ihre Klasse explizit genau dieselbe Semantik wie hatList<T>

Ich sehe oft viele Leute, die tonnenweise zusätzliche Arbeit leisten, nur weil die FxCop-Regel dies sagt oder wenn jemandes Blog sagt, dass es eine "schlechte" Praxis ist. Oft wird dadurch Code zum Entwerfen von Palooza-Verrücktheiten. Behandeln Sie es wie viele Richtlinien als Richtlinie, die Ausnahmen haben kann .

Shital Shah
quelle
4

Obwohl ich nicht wie die meisten dieser Antworten einen komplexen Vergleich habe, möchte ich meine Methode zur Behandlung dieser Situation mitteilen. Durch die Erweiterung IEnumerable<T>können Sie Ihrer TeamKlasse erlauben , Linq-Abfrageerweiterungen zu unterstützen, ohne alle Methoden und Eigenschaften von öffentlich zugänglich zu machen List<T>.

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

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

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

    ...
}

class Player
{
    ...
}
Null511
quelle
3

Wenn Ihre Klassenbenutzer alle Methoden und Eigenschaften benötigen, die ** List hat, sollten Sie Ihre Klasse daraus ableiten. Wenn sie diese nicht benötigen, fügen Sie die Liste bei und erstellen Sie Wrapper für Methoden, die Ihre Klassenbenutzer tatsächlich benötigen.

Dies ist eine strenge Regel, wenn Sie eine öffentliche API oder einen anderen Code schreiben , der von vielen Personen verwendet wird. Sie können diese Regel ignorieren, wenn Sie eine winzige App und nicht mehr als 2 Entwickler haben. Dies spart Ihnen Zeit.

Bei kleinen Apps können Sie auch eine andere, weniger strenge Sprache wählen. Ruby, JavaScript - alles, mit dem Sie weniger Code schreiben können.

Ivan Nikitin
quelle
3

Ich wollte nur hinzufügen, dass Bertrand Meyer, der Erfinder von Eiffel und Design by Contract, von ihm Teamgeerbt hätte, List<Player>ohne auch nur ein Augenlid zu schlagen.

In seinem Buch " Objektorientierte Softwarekonstruktion" beschreibt er die Implementierung eines GUI-Systems, bei dem rechteckige Fenster untergeordnete Fenster haben können. Er hat einfach Windowvon beiden erben Rectangleund Tree<Window>um die Umsetzung wiederverwendet werden .

C # ist jedoch nicht Eiffel. Letzteres unterstützt die Mehrfachvererbung und Umbenennung von Features . Wenn Sie in C # eine Unterklasse erstellen, erben Sie sowohl die Schnittstelle als auch die Implementierung. Sie können die Implementierung überschreiben, aber die Aufrufkonventionen werden direkt aus der Oberklasse kopiert. In Eiffel, jedoch können Sie die Namen der öffentlichen Methoden ändern, so dass Sie umbenennen Addund Removeauf Hireund Firein Ihrem Team. Wenn eine Instanz von Teamzurückgesendet List<Player>wird, verwendet Addund Removeändert der Anrufer sie, aber Ihre virtuellen Methoden Hireund Firewerden aufgerufen.

Alexey
quelle
1
Hier liegt das Problem nicht in der Mehrfachvererbung, den Mixins (usw.) oder dem Umgang mit ihrer Abwesenheit. In jeder Sprache, auch in der menschlichen Sprache, ist ein Team keine Liste von Personen, sondern besteht aus einer Liste von Personen. Dies ist ein abstraktes Konzept. Wenn Bertrand Meyer oder jemand ein Team durch Unterklassifizierung führt, macht List etwas falsch. Sie sollten eine Liste in Unterklassen unterteilen, wenn Sie eine spezifischere Art von Liste wünschen. Hoffe du stimmst zu.
Mauro Sampietro
1
Das hängt davon ab, was Vererbung für Sie ist. An sich ist es eine abstrakte Operation, es bedeutet nicht, dass zwei Klassen in einer "ist eine besondere Art von" Beziehung stehen, obwohl es die Mainstream-Interpretation ist. Die Vererbung kann jedoch als Mechanismus für die Wiederverwendung der Implementierung behandelt werden, wenn das Sprachdesign dies unterstützt, obwohl die Komposition heutzutage die bevorzugte Alternative ist.
Alexey
2

Ich glaube, ich stimme Ihrer Verallgemeinerung nicht zu. Ein Team ist nicht nur eine Ansammlung von Spielern. Ein Team hat so viel mehr Informationen darüber - Name, Emblem, Sammlung von Management- / Verwaltungsmitarbeitern, Sammlung von Trainerteams, dann Sammlung von Spielern. Richtig, Ihre FootballTeam-Klasse sollte 3 Sammlungen haben und selbst keine Sammlung sein. wenn es darum geht, die reale Welt richtig zu modellieren.

Sie könnten eine PlayerCollection-Klasse in Betracht ziehen, die wie die Specialized StringCollection einige andere Funktionen bietet - wie Validierung und Überprüfungen, bevor Objekte zum internen Speicher hinzugefügt oder aus diesem entfernt werden.

Vielleicht passt die Vorstellung einer PlayerCollection besser zu Ihrem bevorzugten Ansatz?

public class PlayerCollection : Collection<Player> 
{ 
}

Und dann kann das FootballTeam so aussehen:

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

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

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

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

Bevorzugen Sie Schnittstellen gegenüber Klassen

Klassen sollten vermeiden, von Klassen abgeleitet zu werden, und stattdessen die erforderlichen minimalen Schnittstellen implementieren.

Vererbung bricht Kapselung

Das Ableiten von Klassen bricht die Kapselung ab :

  • legt interne Details zur Implementierung Ihrer Sammlung offen
  • deklariert eine Schnittstelle (Satz öffentlicher Funktionen und Eigenschaften), die möglicherweise nicht geeignet ist

Dies erschwert unter anderem die Umgestaltung Ihres Codes.

Klassen sind ein Implementierungsdetail

Klassen sind ein Implementierungsdetail, das vor anderen Teilen Ihres Codes verborgen bleiben sollte. Kurz gesagt, a System.Listist eine spezifische Implementierung eines abstrakten Datentyps, der jetzt und in Zukunft angemessen sein kann oder nicht.

Konzeptionell ist die Tatsache, dass der System.ListDatentyp "Liste" heißt, ein bisschen wie ein roter Hering. A System.List<T>ist eine veränderbare geordnete Sammlung, die amortisierte O (1) -Operationen zum Hinzufügen, Einfügen und Entfernen von Elementen und O (1) -Operationen zum Abrufen der Anzahl von Elementen oder zum Abrufen und Festlegen von Elementen nach Index unterstützt.

Je kleiner die Schnittstelle, desto flexibler der Code

Beim Entwerfen einer Datenstruktur ist der Code umso flexibler, je einfacher die Schnittstelle ist. Schauen Sie sich an, wie leistungsfähig LINQ ist, um dies zu demonstrieren.

So wählen Sie Schnittstellen aus

Wenn Sie an "Liste" denken, sollten Sie sich zunächst sagen: "Ich muss eine Sammlung von Baseballspielern repräsentieren." Nehmen wir also an, Sie möchten dies mit einer Klasse modellieren. Zunächst sollten Sie entscheiden, wie viele Schnittstellen diese Klasse mindestens verfügbar machen soll.

Einige Fragen, die diesen Prozess leiten können:

  • Muss ich zählen? Wenn nicht, implementieren SieIEnumerable<T>
  • Wird sich diese Sammlung nach der Initialisierung ändern? Wenn nicht in Betracht ziehen IReadonlyList<T>.
  • Ist es wichtig, dass ich über den Index auf Elemente zugreifen kann? ErwägenICollection<T>
  • Ist die Reihenfolge, in der ich Artikel zur Sammlung hinzufüge, wichtig? Vielleicht ist es ein ISet<T>?
  • Wenn Sie diese Sache wirklich wollen, dann setzen Sie sie fort IList<T>.

Auf diese Weise koppeln Sie keine anderen Teile des Codes mit Implementierungsdetails Ihrer Baseballspieler-Sammlung und können die Implementierung ändern, solange Sie die Benutzeroberfläche respektieren.

Wenn Sie diesen Ansatz wählen, werden Sie feststellen, dass Code leichter zu lesen, umzugestalten und wiederzuverwenden ist.

Hinweise zum Vermeiden von Boilerplate

Die Implementierung von Schnittstellen in einer modernen IDE sollte einfach sein. Klicken Sie mit der rechten Maustaste und wählen Sie "Schnittstelle implementieren". Leiten Sie dann bei Bedarf alle Implementierungen an eine Mitgliedsklasse weiter.

Das heißt, wenn Sie feststellen, dass Sie viel Boilerplate schreiben, liegt dies möglicherweise daran, dass Sie mehr Funktionen verfügbar machen, als Sie sollten. Es ist der gleiche Grund, warum Sie nicht von einer Klasse erben sollten.

Sie können auch kleinere Schnittstellen entwerfen, die für Ihre Anwendung sinnvoll sind, und möglicherweise nur ein paar Hilfserweiterungsfunktionen, um diese Schnittstellen anderen zuzuordnen, die Sie benötigen. Dies ist der Ansatz, den ich in meiner eigenen IArraySchnittstelle für die LinqArray-Bibliothek gewählt habe .

cdiggins
quelle
2

Probleme mit der Serialisierung

Ein Aspekt fehlt. Klassen, die von List <> erben können mit XmlSerializer nicht korrekt serialisiert werden . In diesem Fall muss stattdessen DataContractSerializer verwendet werden, oder es ist eine eigene Serialisierungsimplementierung erforderlich.

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

Weiterführende Literatur: Wenn eine Klasse von List <> geerbt wird, serialisiert XmlSerializer keine anderen Attribute

Sumpf wackeln
quelle
0

Wann ist es akzeptabel?

Um Eric Lippert zu zitieren:

Wenn Sie einen Mechanismus erstellen, der den List<T>Mechanismus erweitert.

Zum Beispiel haben Sie es satt, dass die AddRangeMethode in IList<T>folgenden Fällen fehlt :

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

public class MoreConvenientList<T> : List<T>, IMoreConvenientListInterface<T> { }
Phoog
quelle