it-swarm.com.de

Warum nicht von List <T> erben?

Bei der Planung meiner Programme beginne ich oft mit einer Gedankenreihe, die so aussieht:

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 stelle ich jedoch fest, dass Mannschaften neben der reinen Spielerliste auch andere Eigenschaften haben, die aufgezeichnet werden müssen. Zum Beispiel die laufende Gesamtsumme der Punkte in dieser Saison, das aktuelle Budget, die einheitlichen Farben, ein string, das den Namen der Mannschaft darstellt, usw.

Also dann denke ich:

Okay, eine Fußballmannschaft ist wie eine Liste von Spielern, aber zusätzlich hat sie einen Namen (string) und eine laufende Gesamtsumme von Punkten (int). .NET bietet keine Klasse zum Speichern von Fußballmannschaften, 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 
}

Aber es stellt sich heraus, dass eine Richtlinie besagt, dass Sie nicht von List<T> erben sollten . Diese Richtlinie verwirrt mich in zweierlei Hinsicht zutiefst.

Warum nicht?

Anscheinend List ist irgendwie für die Leistung optimiert . Wie? Welche Leistungsprobleme kann ich verursachen, wenn ich List verlängere? Was genau wird brechen?

Ein weiterer Grund, den ich gesehen habe, ist, dass List von Microsoft bereitgestellt wird und ich keine Kontrolle darüber habe, also ich kann es später nicht ändern, nachdem eine "öffentliche API" verfügbar gemacht wurde . 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 nicht über diese öffentliche API verfügt und dies wahrscheinlich auch nicht sein wird? Wenn ich von List und übernehme, benötige ich eine öffentliche API. Welche Schwierigkeiten habe ich?

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

Und schließlich, wenn Microsoft nicht wollte, dass ich von List übernehme, warum haben sie dann nicht die Klasse sealed erstellt?

Was soll ich sonst noch verwenden?

Anscheinend hat Microsoft für benutzerdefinierte Sammlungen eine Klasse Collection bereitgestellt, die anstelle von List erweitert werden sollte. Aber diese Klasse ist sehr kahl und hat nicht viele nützliche Dinge, zum Beispiel wie AddRange . jvitor83s Antwort liefert eine Leistungsbegründung für diese bestimmte Methode, aber wie ist eine langsame AddRange nicht besser als keine AddRange?

Das Erben von Collection ist weitaus 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 verrichten soll. Ich kann also nicht anders, als etwas falsch zu verstehen, und das Erben von Collection ist eigentlich nicht die richtige Lösung für mein Problem.

Ich habe Vorschläge wie die Implementierung von IList gesehen. Einfach nein. Das sind Dutzende von Codezeilen, die mir nichts bringen.

Zuletzt schlagen einige vor, das List in etwas zu wickeln:

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.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 des internen List weiterleiten ... 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. Du sagst nicht "John McFootballer hat sich SomeTeams Spielern angeschlossen". Sie sagen "John ist SomeTeam beigetreten". Sie fügen "Zeichenfolgen" keinen Buchstaben hinzu, sondern einen Buchstaben. Sie fügen ein Buch nicht zu den Büchern einer Bibliothek hinzu, sondern fügen ein Buch zu einer Bibliothek hinzu.

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

Meine Frage (zusammengefasst)

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

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 von List<T> erbt oder nicht?

1247
Superbest

Hier gibt es einige gute Antworten. Ich möchte ihnen die folgenden Punkte hinzufügen.

Was ist die richtige C # -Darstellung einer Datenstruktur, die "logisch" (dh "für den menschlichen Verstand") nur eine Auflistung von Dingen mit wenigen Handgriffen ist?

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

A football team is a particular kind of _____

Hat jemand "Liste der Fußballspieler mit ein paar Schnickschnack" oder haben alle "Sportmannschaft" oder "Verein" oder "Organisation" gesagt? Ihre Vorstellung, dass eine Fußballmannschaft eine bestimmte Art von Liste von Spielern ist , ist 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 sich in der Geschäftsdomäne des Programms befindet . Mischen Sie diese nicht! Eine Fußballmannschaft ist eine Art Mannschaft; es hat a Dienstplan, ein Dienstplan ist eine Liste von Spielern . Ein Kader ist keine bestimmte Art von Spielerliste . Ein Kader ist eine Liste von Spielern. Erstellen Sie also eine Eigenschaft mit dem Namen Roster, die ein List<Player> ist. Und mach es ReadOnlyList<Player>, während du dabei bist, es sei denn, du glaubst, dass jeder, der etwas über eine Fußballmannschaft weiß, Spieler aus der Liste streichen kann.

Ist das Erben von List<T> immer inakzeptabel?

Für wen nicht akzeptabel? Mich? 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 von List<T> erbt oder nicht?

Baut 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 so zu tippen, dass Sie fünfzigmal mehr Weiterleitungsmethoden für die relevanten Mitglieder von List<T> geschrieben hätten. Sie haben eindeutig keine Angst vor Ausführlichkeit, und wir sprechen hier über eine sehr kleine Menge an Code. Dies ist ein paar Minuten Arbeit.

AKTUALISIEREN

Ich habe mir Gedanken darüber gemacht 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 als mit einer Liste von Spielern zu modellieren. Das Problem mit einer Mannschaft als/mit einer Liste von Spielern ist, dass Sie eine Momentaufnahme der Mannschaft zu einem bestimmten Zeitpunkt 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 gerne fragen, wie viele Seahawks-Spieler Spiele aufgrund von Verletzungen zwischen 2003 und 2013 verpasst haben. oder "Welcher Denver-Spieler, der zuvor für eine andere Mannschaft gespielt hatte, verzeichnete im Jahresvergleich den größten Zuwachs an Werften?" 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, beispielsweise als ein Spieler angeworben, verletzt, in Rente gegangen usw. Offensichtlich ist dies die aktuelle Spielerliste Dies ist eine wichtige Tatsache, die wahrscheinlich im Vordergrund stehen sollte, aber es kann auch andere interessante Dinge geben, die Sie mit diesem Objekt tun möchten und die eine historischere Perspektive erfordern.

1417
Eric Lippert

Zuletzt schlagen einige vor, die Liste in etwas einzuschließen:

Das ist der richtige Weg. "Überflüssig wortreich" ist eine schlechte Sichtweise. Es hat eine explizite Bedeutung, wenn Sie my_team.Players.Count schreiben. Sie möchten die Spieler zählen.

my_team.Count

..bedeutet nichts. Was zählen?

Ein Team ist keine Liste - es besteht aus mehr als nur einer Liste von Spielern. Ein Team besitzt Spieler, daher sollten Spieler ein Teil davon sein (ein Mitglied).

Wenn Sie wirklich besorgt sind, dass es zu ausführlich ist, können Sie jederzeit Eigenschaften aus dem Team anzeigen:

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

..was wird:

my_team.PlayerCount

Dies hat jetzt Bedeutung und folgt The Law Of Demeter .

Beachten Sie auch das Composite-Wiederverwendungsprinzip . Indem Sie von List<T> erben, sagen Sie, dass ein Team eine Liste von Spielern ist und überflüssige Methoden aufdeckt. Das ist falsch - wie Sie sagten, ist ein Team mehr als eine Liste von Spielern: Es hat einen Namen, Manager, Vorstandsmitglieder, Trainer, medizinisches Personal, Gehaltsobergrenzen usw. Indem Ihre Teamklasse eine Liste von Spielern enthält, haben Sie sagen "Ein Team hat eine Liste von Spielern", aber es kann auch andere Dinge haben.

274
Simon Whitehead

Wow, dein Post hat eine ganze Reihe von Fragen und Punkten. Die meisten Argumente, die Sie von Microsoft erhalten, sind genau richtig. Beginnen wir mit allem über List<T>

  • List<T> ist stark optimiert. Seine Hauptverwendung ist es, 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 var list = new MyList<int, string>();.
  • CA1002: Machen Sie keine generischen Listen verfügbar : Grundsätzlich lohnt es sich, diese App mit guten Codierungspraktiken zu entwickeln, auch wenn Sie sie als einziger Entwickler verwenden möchten. Sie können die Liste weiterhin als IList<T> verfügbar machen, wenn Sie für einen Verbraucher eine indizierte Liste benötigen. Auf diese Weise können Sie später die Implementierung innerhalb einer Klasse ändern.
  • Microsoft hat Collection<T> sehr allgemein gehalten, da es sich um ein allgemeines Konzept handelt ... der Name sagt schon alles; Es ist nur eine Sammlung. Es gibt genauere Versionen wie SortedCollection<T>, ObservableCollection<T>, ReadOnlyCollection<T> usw., die jeweils IList<T> implementieren, aber nicht List<T>.
  • Collection<T> ermöglicht das Überschreiben von Mitgliedern (d. H. 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 Zusammensetzung vs 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 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.
}
140
m-y
class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal;
}

Früherer Code bedeutet: Ein paar Leute von der Straße, die Fußball spielen und zufällig einen Namen haben. So etwas wie:

People playing football

Wie auch immer, dieser Code (aus m-ys Antwort)

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

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

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

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

Das heißt: Dies ist eine Fußballmannschaft mit Management, Spielern, Admins usw. Etwas wie:

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

So wird Ihre Logik in Bildern dargestellt ...

131
Nean Der Thal

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?.

Indem Sie List erweitern, beschränken Sie sich auf verschiedene Arten:

  1. Sie können den Zugriff nicht einschränken (z. B. das Stoppen von Personen, die den Dienstplan ändern). Sie erhalten alle List-Methoden, unabhängig davon, ob Sie sie alle benötigen 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 eigene Listen.

  3. Sie beschränken Ihre Vererbungsmöglichkeiten. Zum Beispiel möchten Sie vielleicht ein generisches Team-Objekt erstellen und dann über BaseballTeam, FootballTeam usw. verfügen, die davon erben. Um von List zu erben, müssen Sie die Vererbung von Team ausführen. Dies bedeutet jedoch, dass alle verschiedenen Teamtypen die gleiche Implementierung dieses Dienstplans haben müssen.

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

Vererbung - Ihr Objekt wird zu einer Instanz des Objekts, das das gewünschte Verhalten aufweist.

Beide haben ihre Verwendung, aber dies ist ein klarer Fall, bei dem die Zusammensetzung bevorzugt wird.

103
Tim B

Wie jeder betont hat, ist eine Mannschaft von Spielern keine Liste von Spielern. Dieser Fehler wird von vielen Menschen überall begangen, vielleicht auf verschiedenen Fachgebieten. Oft ist das Problem subtil und manchmal, wie in diesem Fall, sehr grob. Solche Entwürfe sind schlecht, weil sie das Liskov-Substitutionsprinzip verletzen. 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 Parent/Child-Beziehung zwischen Klassen beibehalten werden müssen:

  • ein Kind sollte keine Merkmale benötigen, die unter dem liegen, was den Elternteil vollständig definiert.
  • ein Elternteil sollte kein Merkmal zusätzlich zu dem benötigen, was das Kind vollständig definiert.

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

Hier ist ein Weg, um eine Lösung zu durchdenken und das obige Prinzip anzuwenden, das helfen sollte, einen solchen Fehler zu vermeiden. Man sollte seine Hypothese testen, indem man überprüft, ob alle Operationen einer Elternklasse 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 in derselben Bedeutung zu?)
    • Ist ein Team eine Ansammlung homogener Einheiten? Ja, Team ist eine Ansammlung von Spielern
    • Beschreibt die Reihenfolge der Einbeziehung der Spieler den Zustand der Mannschaft und stellt die Mannschaft 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/fallengelassen werden? Nein

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

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

62
Satyan Raina

Was ist, wenn das FootballTeam neben dem Hauptteam ein Reserveteam 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 eine und nicht ist eine .

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.

Ist Ihr SomethingCollection semantisch sinnvoll? Tun Sie dies nur, wenn Ihr Typ eine Sammlung von Something ist.

Im Fall von FootballTeam klingt es nicht richtig. Ein Team ist mehr als ein Collection. Ein Team kann Trainer, Ausbilder 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 Ansammlung von Spielern, die einen gültigen Namen für eine Klasse darstellen, die von List<FootballPlayer> erbt, wenn Sie das wirklich wollten.

Wirklich List<FootballPlayer> ist ein perfekter Typ, mit dem man umgehen kann. Vielleicht IList<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?

46
Sam Leach

Design> Implementierung

Welche Methoden und Eigenschaften Sie verfügbar machen, hängt von der Entwurfsentscheidung ab. Welche Basisklasse Sie erben, ist ein Implementierungsdetail. Ich denke, es lohnt sich, einen Schritt zurück zu ersteren zu gehen.

Ein Objekt ist eine Sammlung von Daten und Verhaltensweisen.

Deine ersten Fragen sollten also sein:

  • Welche Daten enthält dieses Objekt in dem von mir erstellten Modell?
  • Welches Verhalten zeigt dieses Objekt in diesem Modell?
  • Wie könnte sich dies in Zukunft ändern?

Denken Sie daran, dass Vererbung eine "isa" (is a) -Beziehung impliziert, während Komposition eine "has a" (hasa) -Beziehung impliziert. Wählen Sie aus Ihrer Sicht die richtige für Ihre Situation aus, und berücksichtigen Sie dabei, wo sich die Situation bei der Entwicklung Ihrer Anwendung entwickeln könnte.

Denken Sie über Schnittstellen nach, 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 bei der täglichen Codierung bewusst auf dieser Ebene tut. Aber wenn Sie über diese Art von Thema nachdenken, treten Sie in Designgewässern auf. Sich dessen bewusst zu sein, kann befreiend sein.

Berücksichtigen Sie die Konstruktionsmerkmale

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 sie mit einer Fußballmannschaft machen wollen?

Macht footballTeam.Reverse () für Sie Sinn? Sieht footballTeam.ConvertAll <TOutput> () so aus, wie Sie es möchten?

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

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

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

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

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

Möglicherweise haben Sie das Gefühl, dass keine dieser Schnittstellen in Ihrem Modell von Nutzen 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 kategorisch und definitiv falsch ist, ist in jedem Fall fehlgeleitet. Jeder, der versucht, es Ihnen zu sagen, ist kategorisch und definitiv richtig in jedem Fall 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 beinhaltet, von welchen konkreten Klassen Sie durch Vererbung oder Komposition abhängig sind.

Dies ist vielleicht kein großer Schritt, und die Leute bringen oft Design und Implementierung in Konflikt, da es durchaus möglich ist, in ein oder zwei Sekunden alles im Kopf durchzugehen und loszuschreiben.

Ein Gedankenexperiment

Ein künstliches Beispiel: Wie andere bereits erwähnt haben, ist eine Mannschaft 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 möglicherweise auch eine Sammlung von Mitarbeitern und/oder eine Sammlung von Ergebnissen. Dann endest du mit:

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

Ungeachtet des Designs können Sie an dieser Stelle in C # nicht all dies 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 Auflistung über die Vererbung und einer über die Komposition fühlt sich wahrscheinlich an schmutzig. 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. Bauchgefühl beim Hinunterdenken dieser Allee kann Ihnen gut sagen, dass es sich falsch anfühlt, in diese Richtung zu gehen (und zu Recht oder zu Unrecht, Ihre Kollegen könnten es auch tun, wenn Sie es auf diese Weise implementiert haben!)

Die kurze Antwort (zu spät)

Die Richtlinie zum Nicht-Erben von Auflistungsklassen ist nicht C # -spezifisch. Sie wird in vielen Programmiersprachen verwendet. 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 oftmals die Vererbung überwindet. 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 verstrichen, insbesondere im Laufe der Zeit und hinsichtlich 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 andeutend sein.

33
El Zorko

Erstens hat es mit Usability zu tun. Wenn Sie die Vererbung verwenden, legt die Klasse Team Verhalten (Methoden) offen, die ausschließlich für die Objektmanipulation entwickelt wurden. Beispielsweise machen die Methoden AsReadOnly() oder CopyTo(obj) für das Teamobjekt keinen Sinn. Anstelle der Methode AddRange(items) möchten Sie wahrscheinlich eine aussagekräftigere Methode AddPlayers(players).

Wenn Sie LINQ verwenden möchten, ist die Implementierung einer generischen Schnittstelle wie ICollection<T> oder IEnumerable<T> sinnvoller.

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

31
Dmitry S.

Eine Fußballmannschaft ist nicht eine Liste von Fußballspielern. Eine Fußballmannschaft setzt sich zusammen 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 
}
26
Mauro Sampietro

Lassen Sie mich Ihre Frage umschreiben. Vielleicht sehen Sie das Thema aus einer anderen Perspektive.

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

string team = new string();

Später stellte ich fest, dass Teams auch Spieler haben.

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

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

Anschließend können Sie überprüfen, ob Eigenschaften für andere Klassen freigegeben sind. Und denk an Vererbung.

24
user1852503

Es kommt auf den Kontext an

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 Zusammenhang richtig. 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 ehemaligen Spieler usw.

Unklare Semantik

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

Klassen sind erweiterbar

Wenn Sie eine Klasse mit nur einem Mitglied verwenden (z. B. IList activePlayers), können Sie den Namen des Mitglieds (und zusätzlich dessen 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 Classloader geladen werden und wird von der virtuellen Maschine zwischengespeichert. Dies kostet Sie Laufzeitleistung und Speicher. Wenn Sie einen 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 eine IList verwenden, keine davon abgeleitete Klasse.

Schlussfolgerung/Überlegungen

Wenn Sie einen bestimmten Kontext haben, ist es in Ordnung, ein Team als eine Liste von Spielern zu betrachten. Zum Beispiel ist es in 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, bei der nicht klar ist, in welchem ​​Kontext sie zukünftig verwendet werden kann. 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 die Mannschaft nicht als eine Liste von Spielern betrachten, aber Sie sollten die Liste der (derzeit aktiven, nicht verletzten usw.) Spieler als ein Attribut der Mannschaft modellieren.

22

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

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

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

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

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 die Richtlinien, auf die verwiesen wird, die ".NET Framework Design Guidelines" und nicht die ".NET Application Design Guidelines" sind. Es gibt einen Unterschied, und im Allgemeinen ist das öffentliche API-Design viel strenger.

Kann ich diese Richtlinie ignorieren, wenn mein aktuelles Projekt nicht über diese öffentliche API verfügt und dies wahrscheinlich auch nicht sein wird? Welche Schwierigkeiten habe ich, wenn ich von List übernehme und eine öffentliche API benötige?

Ziemlich genau. Sie können die dahinter stehenden Überlegungen anstellen, um festzustellen, ob sie für Ihre Situation zutreffen. Wenn Sie jedoch keine öffentliche API erstellen, müssen Sie sich keine besonderen Gedanken über API - Probleme wie die Versionsverwaltung machen Teilmenge).

Wenn Sie in Zukunft eine öffentliche API hinzufügen, müssen Sie entweder Ihre API von Ihrer Implementierung abstrahieren (indem Sie Ihren List<T> nicht direkt verfügbar machen) oder die Richtlinien mit den möglichen zukünftigen Problemen verletzen, die dies mit sich bringt.

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

Hängt vom Kontext ab, aber da wir FootballTeam als Beispiel verwenden - stellen Sie sich vor, dass Sie kein FootballPlayer hinzufügen können, wenn 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 override Add nicht, weil es nicht virtual ist (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 auf die Verwendung von IList<T> wechseln und alle Kompilierungsfehler 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. Tun Sie für private APIs, was Sie wollen.

18
Mark Brackett

Erlaubt es den Leuten zu sagen

myTeam.subList(3, 5);

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

16
Cruncher

Es 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 stellen Sie möglicherweise fest, dass Sie weiterhin Code duplizieren, der in der Liste durchläuft. Zu diesem Zeitpunkt haben Sie die Möglichkeit, ein FootballTeam-Objekt zu erstellen, das die Liste der Spieler umschließt. In der FootballTeam-Klasse wird der gesamte Code der Spielerliste gespeichert.

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. Soweit Ihre Kunden wissen, kann dies implementiert werden, indem Sie die Liste der Spieler in einer Datenbank nachschlagen. Sie müssen es nicht wissen, und das verbessert Ihr Design.

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

Genau :) Du wirst footballTeam.Add (john) sagen, nicht footballTeam.List.Add (john). Die interne Liste ist nicht sichtbar.

15
xpmatteo

Hier gibt es viele ausgezeichnete Antworten, aber ich möchte auf etwas eingehen, das ich nicht erwähnt habe: Objektorientiertes Design handelt von Befähigung von Objekten .

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. Tatsächlich möchten Sie einen Schritt weiter gehen und aktiv verhindern, dass andere Objekte diese Interna umgehen.

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

Angenommen, Sie möchten unterscheiden, wann ein Spieler abreist, indem Sie wissen, ob er zurückgetreten ist, zurückgetreten ist oder gefeuert wurde. Sie können eine RemovePlayer -Methode implementieren, die eine entsprechende Eingabe-Enumeration verwendet. Wenn Sie jedoch von List erben, können Sie den direkten Zugriff auf Remove, RemoveAll und sogar Clear nicht verhindern. Infolgedessen haben Sie tatsächlich Ihre Klasse FootballTeam entmachtet .


Zusätzliche Überlegungen 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, wenn alle Kunden Ihr Team einsetzen würden. Dieses Problem ist jedoch sehr gering im Vergleich zu der Tatsache, dass Sie List Players allen und jedem ausgesetzt haben, damit sie ohne Ihre Zustimmung mit Ihrem Team spielen können.

Sie fahren fort zu sagen:

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

Sie irren sich in Bezug auf das erste Bit: Löschen Sie die Wortliste, und es ist tatsächlich offensichtlich, dass ein Team Spieler hat.
Sie haben jedoch mit der Sekunde den Nagel auf den Kopf getroffen. Sie möchten nicht, dass Clients ateam.Players.Add(...) aufrufen. Sie möchten, dass sie ateam.AddPlayer(...) aufrufen. Und Ihre Implementierung würde (möglicherweise unter anderem) Players.Add(...) intern aufrufen.


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

13
Disillusioned

Was ist die korrekte C # -Darstellung einer Datenstruktur ...

Erinnern Sie sich: "Alle Modelle sind falsch, aber einige sind nützlich." - George E. P. Box

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

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 für Methoden verfügbar machen, die möglicherweise nicht verfügbar gemacht werden sollten, vor allem, weil FootballTeam wie eine Geschäftseinheit aussieht.

- Ausgabe 2

Ich kann mich aufrichtig nicht an das erinnern, worauf ich mich im Kommentar „Überentwickle nicht“ bezog. Obwohl ich der Meinung bin, dass die Denkweise KISS ein guter Leitfaden ist, möchte ich betonen, dass das Erben einer Business Class von List aufgrund Abstraktionsverlust mehr Probleme verursachen würde, als es löst.

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.

12
ArturoTena

Das erinnert mich an das "Ist ein" gegenüber "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 sie geerbt haben, als Elementvariable 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 durch das Erben von der Klasse entstehen können.

Welches machst du? Wie bei vielen Dingen ... kommt es auf den Kontext an. Der Leitfaden, den ich verwenden würde, ist, dass, um von einer anderen Klasse zu erben, es wirklich eine "ist eine" Beziehung geben sollte. Wenn Sie also eine Klasse namens BMW schreiben, könnte sie von Car erben, weil ein BMW wirklich ein Auto ist. Eine Horse-Klasse kann von der Mammal-Klasse erben, da ein Pferd im wirklichen Leben ein Säugetier ist und jede Mammal-Funktionalität für Horse relevant sein sollte. Aber können Sie sagen, dass ein Team eine Liste ist? Nach allem, was ich sagen kann, scheint es nicht so, als ob ein Team wirklich eine Liste ist. In diesem Fall hätte ich also eine Liste als Mitgliedsvariable.

10

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")

Außerdem kann meine PlayerCollection von anderen Klassen wie "Club" verwendet werden, ohne dass Code dupliziert wird.

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 die Schuld gibt, fragen Sie sich, was lesbarer und wartbarer ist.

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 verhindern, dass Sie List <T> in Ihrem realen Anwendungsfall verwenden. Fügen Sie jedoch keine Einschränkung hinzu, die nicht existieren sollte. Wenn Microsoft das Warum nicht dokumentiert hat, handelt es sich mit Sicherheit um eine "bewährte Methode", die aus dem Nichts stammt.

9
Nicolas Dorier

Die Richtlinien besagen, dass die öffentliche API nicht die interne Entwurfsentscheidung offenlegen sollte, ob Sie eine Liste, eine Menge, 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.

8
QuentinUK

Wenn sie sagen, List<T> ist "optimiert" Ich denke, sie wollen damit meinen, dass es keine Funktionen wie virtuelle Methoden gibt, die etwas teurer sind. Das Problem ist also, dass Sie verlieren, wenn Sie List<T> in Ihrer öffentlichen API verfügbar machen Möglichkeit, Geschäftsregeln durchzusetzen oder ihre 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 zur Verfügung steht), ist es möglicherweise in Ordnung, wenn Sie Zeit sparen und die gewünschte Funktionalität verwenden Duplikat. Der Vorteil des Erbens von List<T> besteht darin, dass Sie eine Menge dummen Wrapper-Code eliminieren, der in absehbarer Zukunft nicht mehr angepasst werden kann. Auch wenn Sie möchten, dass Ihre Klasse explizit genau die gleiche Semantik wie List<T> für die Lebensdauer Ihrer APIs hat, ist dies möglicherweise auch in Ordnung.

Ich sehe oft viele Leute, die Tonnen von zusätzlicher Arbeit erledigen, nur weil es die FxCop-Regel vorschreibt, oder weil in einem Blog jemand sagt, es sei eine "schlechte" Praxis. Dies führt oft zu Code, der das Muster "Palooza Weirdness" entwirft. Behandeln Sie es wie viele Richtlinien als Richtlinie, die Ausnahmen haben kann .

6
Shital Shah

Obwohl ich keinen komplexen Vergleich habe, wie die meisten dieser Antworten, möchte ich meine Methode für den Umgang mit dieser Situation teilen. Indem Sie IEnumerable<T> erweitern, können Sie zulassen, dass Ihre Team -Klasse Linq-Abfrageerweiterungen unterstützt, ohne alle Methoden und Eigenschaften von List<T> öffentlich verfügbar zu machen.

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

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

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

    ...
}

class Player
{
    ...
}
4
Null511

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 hinzu und erstellen Sie Wrapper für Methoden, die Ihre Klassenbenutzer tatsächlich benötigen.

Dies ist eine strenge Regel, wenn Sie ein öffentliches API oder einen anderen Code schreiben, der von vielen Leuten verwendet wird. Sie können diese Regel ignorieren, wenn Sie eine winzige App und nicht mehr als 2 Entwickler haben. So sparen Sie etwas Zeit.

Für winzige Apps können Sie auch eine andere, weniger strenge Sprache wählen. Ruby, JavaScript - alles, was Sie weniger Code schreiben können.

3
Ivan Nikitin

Ich wollte nur hinzufügen, dass Bertrand Meyer, der Erfinder von Eiffel und Design by Contract, Team von List<Player> erben lassen würde, ohne auch nur ein Augenlid zu schlagen.

In seinem Buch Object-Oriented Software Construction beschreibt er die Implementierung eines GUI-Systems, bei dem rechteckige Fenster untergeordnete Fenster haben können. Er muss einfach Window von Rectangle und Tree<Window> erben, um die Implementierung wiederzuverwenden.

C # ist jedoch nicht Eiffel. Letzteres unterstützt Mehrfachvererbung und mbenennen 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 Superklasse kopiert. In Eiffel können Sie jedoch die Namen der öffentlichen Methoden ändern, sodass Sie Add und Remove in Hire und Fire in Ihrem Team umbenennen können. Wenn eine Instanz von Team auf List<Player> zurückgesendet wird, verwendet der Anrufer Add und Remove, um sie zu ändern, aber Ihre virtuellen Methoden Hire und Fire wird aufgerufen.

3
Alexey

Ich glaube, ich stimme Ihrer Verallgemeinerung nicht zu. Eine Mannschaft ist nicht nur eine Ansammlung von Spielern. Ein Team hat so viel mehr Informationen darüber - Name, Emblem, Sammlung von Führungskräften/Administratoren, Sammlung von Trainern, dann Sammlung von Spielern. Richtigerweise sollte Ihre FootballTeam-Klasse 3 Sammlungen haben und nicht selbst eine Sammlung sein. wenn es darum geht, die reale Welt richtig zu modellieren.

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

Vielleicht passt die Idee eines PlayerCollection-Betters 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();
}
0
Eniola

Bevorzugen Sie Schnittstellen gegenüber Klassen

Klassen sollten das Ableiten von Klassen vermeiden und stattdessen die erforderlichen minimalen Schnittstellen implementieren.

Vererbung unterbricht die Kapselung

Ableiten von Klassen nterbricht die Kapselung :

  • zeigt interne Details zur Implementierung Ihrer Sammlung an
  • deklariert eine Schnittstelle (eine Reihe von öffentlichen 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 sein sollte. Kurz gesagt, ein _System.List_ ist eine spezifische Implementierung eines abstrakten Datentyps, der jetzt und in Zukunft geeignet sein kann oder auch nicht.

Konzeptionell ist die Tatsache, dass der Datentyp System.List als "Liste" bezeichnet wird, ein bisschen wie ein Hering. Ein _System.List<T>_ ist eine änderbar geordnete Sammlung, die amortisierte O(1) Vorgänge zum Hinzufügen, Einfügen und Entfernen von Elementen und O(1) Vorgänge zum Abrufen der Nummer unterstützt von Elementen oder Abrufen und Setzen von Elementen nach Index.

Je kleiner die Schnittstelle, desto flexibler der Code

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

So wählen Sie Schnittstellen aus

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

Einige Fragen, die diesen Prozess unterstützen können:

  • Muss ich die Zählung haben? Wenn nicht, implementieren Sie IEnumerable<T>
  • Wird sich diese Sammlung nach der Initialisierung ändern? Wenn nicht, ziehen Sie IReadonlyList<T> in Betracht.
  • Ist es wichtig, dass ich über einen Index auf Elemente zugreifen kann? Betrachten Sie ICollection<T>
  • Ist die Reihenfolge, in der ich Elemente zur Sammlung hinzufüge, wichtig? Vielleicht ist es ein ISet<T> ?
  • Wenn Sie diese Dinge wirklich wollen, setzen Sie IList<T> um.

Auf diese Weise werden Sie andere Teile des Codes nicht mit Implementierungsdetails Ihrer Baseball-Spieler-Sammlung koppeln und können die Implementierungsart ändern, solange Sie die Benutzeroberfläche einhalten.

Auf diese Weise können Sie den Code leichter lesen, umgestalten und wiederverwenden.

Hinweise zur Vermeidung von Boilerplate

Das Implementieren von Schnittstellen in einem modernen IDE sollte einfach sein. Klicken Sie mit der rechten Maustaste und wählen Sie "Schnittstelle implementieren". Leiten Sie dann alle Implementierungen an eine Memberklasse weiter, wenn dies erforderlich ist.

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 IArray - Schnittstelle für die LinqArray-Bibliothek gewählt habe.

0
cdiggins

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

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

Weitere Informationen: Wenn eine Klasse von List <> geerbt wird, serialisiert XmlSerializer keine anderen Attribute.

0
marsh-wiggle