Perché non ereditare dall'elenco <T>?


1400

Quando pianifico i miei programmi, spesso inizio con una catena di pensieri in questo modo:

Una squadra di calcio è solo un elenco di giocatori di calcio. Pertanto, dovrei rappresentarlo con:

var football_team = new List<FootballPlayer>();

L'ordinamento di questo elenco rappresenta l'ordine in cui i giocatori sono elencati nel roster.

Ma mi rendo conto in seguito che le squadre hanno anche altre proprietà, oltre alla semplice lista di giocatori, che devono essere registrate. Ad esempio, il totale corrente dei punteggi in questa stagione, il budget corrente, i colori uniformi, un stringrappresentante del nome della squadra, ecc.

Quindi penso:

Va bene, una squadra di calcio è proprio come un elenco di giocatori, ma ha anche un nome (a string) e un totale parziale di punteggi (a int). .NET non fornisce una classe per l'archiviazione delle squadre di calcio, quindi farò la mia classe. La struttura esistente più simile e pertinente è List<FootballPlayer>, quindi erediterò da essa:

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

Ma si scopre che una linea guida dice che non dovresti ereditareList<T> . Sono completamente confuso da questa linea guida sotto due aspetti.

Perchè no?

Apparentemente Listè in qualche modo ottimizzato per le prestazioni . Come mai? Quali problemi di prestazioni causerò se estendo List? Cosa si romperà esattamente?

Un altro motivo che ho visto è che Listviene fornito da Microsoft e non ho alcun controllo su di esso, quindi non posso cambiarlo in seguito, dopo aver esposto una "API pubblica" . Ma faccio fatica a capirlo. Che cos'è un'API pubblica e perché dovrei preoccuparmi? Se il mio progetto attuale non ha e non è probabile che abbia questa API pubblica, posso tranquillamente ignorare questa linea guida? Se eredito da List e risulta che ho bisogno di un'API pubblica, quali difficoltà avrò?

Perché è ancora importante? Un elenco è un elenco. Cosa potrebbe cambiare? Cosa potrei voler cambiare?

E infine, se Microsoft non voleva che io ereditassi List, perché non hanno fatto la classe sealed?

Cos'altro dovrei usare?

Apparentemente, per le raccolte personalizzate, Microsoft ha fornito una Collectionclasse che dovrebbe essere estesa anziché List. Ma questa classe è molto spoglia e non ha molte cose utili, comeAddRange ad esempio. La risposta di jvitor83 fornisce una logica delle prestazioni per quel particolare metodo, ma come è un lento AddRangenon migliore di no AddRange?

Ereditare da Collectionè molto più lavoro che ereditare Liste non vedo alcun beneficio. Sicuramente Microsoft non mi direbbe di fare un lavoro extra senza motivo, quindi non posso fare a meno di sentirmi in qualche modo fraintendere qualcosa e ereditare non Collectionè in realtà la soluzione giusta per il mio problema.

Ho visto suggerimenti come l'implementazione IList. Proprio no. Sono dozzine di righe di codice boilerplate che non mi fanno guadagnare nulla.

Infine, alcuni suggeriscono di avvolgere Listqualcosa in:

class FootballTeam 
{ 
    public List<FootballPlayer> Players; 
}

Ci sono due problemi con questo:

  1. Rende inutilmente prolisso il mio codice. Ora devo chiamare my_team.Players.Countinvece di solo my_team.Count. Per fortuna, con C # posso definire gli indicizzatori per rendere trasparente l'indicizzazione e inoltrare tutti i metodi degli interni List... Ma questo è un sacco di codice! Cosa ottengo per tutto quel lavoro?

  2. Semplicemente non ha alcun senso. Una squadra di calcio non "ha" un elenco di giocatori. Si è la lista dei giocatori. Non dici "John McFootballer si è unito ai giocatori di SomeTeam". Dici "John è entrato in SomeTeam". Non aggiungi una lettera ai "caratteri di una stringa", aggiungi una lettera a una stringa. Non aggiungi un libro ai libri di una biblioteca, aggiungi un libro a una biblioteca.

Mi rendo conto che si può dire che ciò che accade "sotto il cofano" sia "l'aggiunta di X all'elenco interno di Y", ma questo sembra un modo molto controintuitivo di pensare al mondo.

La mia domanda (riassunta)

Qual è la corretta C # modo di rappresentare una struttura di dati, che, "logicamente" (vale a dire, "per la mente umana") è solo una listdelle thingscon alcune campane e fischietti?

L'ereditarietà è List<T>sempre inaccettabile? Quando è accettabile? Perché perché no? Cosa deve considerare un programmatore quando decide se ereditare List<T>o no?


90
Quindi, la tua domanda è davvero quando usare l'ereditarietà (un quadrato è un rettangolo perché è sempre ragionevole fornire un quadrato quando è stato richiesto un rettangolo generico) e quando usare la composizione (un FootballTeam ha un elenco di giocatori di calcio, inoltre ad altre proprietà che sono altrettanto "fondamentali", come un nome)? Altri programmatori sarebbero confusi se altrove nel codice passasse loro un FootballTeam mentre si aspettavano un semplice Elenco?
Volo Odyssey,

77
cosa succede se ti dico che una squadra di calcio è una società commerciale che PROPRIE un elenco di contratti di giocatori anziché un elenco nudo di giocatori con alcune proprietà aggiuntive?
STT LCU,

75
È davvero abbastanza semplice. Una squadra di calcio è un elenco di giocatori? Ovviamente no, perché come dici tu, ci sono altre proprietà rilevanti. Una squadra di calcio ha un elenco di giocatori? Sì, quindi dovrebbe contenere un elenco. A parte questo, la composizione è semplicemente preferibile all'eredità in generale, perché è più facile modificarla in seguito. Se trovi l'ereditarietà e la composizione logiche allo stesso tempo, scegli la composizione.
Tobberoth,

45
@FlightOdyssey: supponi di avere un metodo SquashRectangle che prende un rettangolo, dimezza la sua altezza e raddoppia la sua larghezza. Ora passa un rettangolo che sembra essere 4x4 ma è di tipo rettangolo e un quadrato che è 4x4. Quali sono le dimensioni dell'oggetto dopo la chiamata a SquashRectangle? Per il rettangolo chiaramente è 2x8. Se il quadrato è 2x8, non è più un quadrato; se non è 2x8, la stessa operazione sulla stessa forma produce un risultato diverso. La conclusione è che un quadrato mutevole non è una specie di rettangolo.
Eric Lippert,

29
@Gangnus: la tua affermazione è semplicemente falsa; è necessaria una sottoclasse reale per avere funzionalità che è un superset della superclasse. A stringè richiesto per fare tutto ciò che si objectpuò fare e altro ancora .
Eric Lippert,

Risposte:


1567

Ci sono alcune buone risposte qui. Aggiungerei loro i seguenti punti.

Qual è il modo corretto di rappresentare una struttura di dati in C # che, "logicamente" (vale a dire "alla mente umana") è solo un elenco di cose con qualche campana e fischietto?

Chiedi a una decina di persone non programmatore che hanno familiarità con l'esistenza del calcio di riempire il campo vuoto:

Una squadra di calcio è un tipo particolare di _____

Forse qualcuno dire "lista di giocatori di calcio con un paio di campane e fischietti", o hanno tutti "sport di squadra" dire o "club" o "organizzazione"? La tua idea che una squadra di calcio sia un particolare tipo di elenco di giocatori è nella tua mente umana e solo nella tua mente umana.

List<T>è un meccanismo . La squadra di calcio è un oggetto business , ovvero un oggetto che rappresenta un concetto che rientra nel dominio aziendale del programma. Non mescolarli! Una squadra di calcio è una specie di squadra; essa ha un ruolo, un roster è una lista dei giocatori . Un elenco non è un tipo particolare di elenco di giocatori . Un elenco è un elenco di giocatori. Quindi crea una proprietà chiamata Rosterche è a List<Player>. E fallo ReadOnlyList<Player>mentre ci sei, a meno che tu non creda che tutti quelli che conoscono una squadra di calcio possano eliminare i giocatori dal roster.

L'ereditarietà è List<T>sempre inaccettabile?

Inaccettabile per chi? Me? No.

Quando è accettabile?

Quando stai costruendo un meccanismo che estende il List<T>meccanismo .

Cosa deve considerare un programmatore quando decide se ereditare List<T>o no?

Sto costruendo un meccanismo o un oggetto business ?

Ma è un sacco di codice! Cosa ottengo per tutto quel lavoro?

Hai speso più tempo a scrivere la tua domanda che ti ci sarebbe voluto scrivere metodi di inoltro per i membri interessati di List<T>cinquanta volte. Chiaramente non hai paura della verbosità, e qui stiamo parlando di una piccolissima quantità di codice; questo è un lavoro di pochi minuti.

AGGIORNARE

Ci ho pensato un po 'di più e c'è un altro motivo per non modellare una squadra di calcio come un elenco di giocatori. In effetti, potrebbe essere una cattiva idea modellare una squadra di calcio con un elenco di giocatori. Il problema con una squadra come / avere un elenco di giocatori è che quello che hai è un'istantanea della squadra in un momento nel tempo . Non so quale sia il tuo business case per questa classe, ma se avessi una classe che rappresentava una squadra di calcio, vorrei porre domande come "Quanti giocatori di Seahawks hanno perso partite a causa di un infortunio tra il 2003 e il 2013?" o "Quale giocatore di Denver, che in precedenza aveva giocato per un'altra squadra, ha avuto il maggiore aumento di iarde su base annua?" o " I Piggers sono andati fino in fondo quest'anno? "

Cioè, una squadra di calcio mi sembra ben modellata come una raccolta di fatti storici come quando un giocatore è stato reclutato, infortunato, in pensione, ecc. Ovviamente l'attuale elenco di giocatori è un fatto importante che probabilmente dovrebbe essere front-and- al centro, ma potrebbero esserci altre cose interessanti che vuoi fare con questo oggetto che richiedono una prospettiva più storica.


84
Anche se le altre risposte sono state di grande aiuto, penso che questo affronti le mie preoccupazioni più direttamente. Per quanto riguarda l'esagerazione della quantità di codice, hai ragione nel dire che alla fine non funziona molto, ma mi confondo molto facilmente quando includo del codice nel mio programma senza capire perché lo sto includendo.
Superbo

128
@Superbest: Sono contento di poterti aiutare! Hai ragione ad ascoltare quei dubbi; se non capisci perché stai scrivendo del codice, o capiscilo o scrivi codice diverso che tu capisca.
Eric Lippert,

48
@Mehrdad Ma sembra che tu dimentichi le regole d'oro dell'ottimizzazione: 1) non farlo, 2) non farlo ancora, e 3) non farlo senza prima fare profili delle prestazioni per mostrare ciò che deve essere ottimizzato.
AJMansfield,

107
@Mehrdad: Onestamente, se la tua applicazione richiede che ti interessi del peso delle prestazioni, per esempio, dei metodi virtuali, allora qualsiasi linguaggio moderno (C #, Java, ecc.) Non è la lingua per te. Ho un laptop proprio qui che potrebbe eseguire una simulazione di ogni gioco arcade che ho giocato da bambino contemporaneamente . Questo mi permette di non preoccuparmi di un livello di riferimento indiretto qua e là.
Eric Lippert,

44
@Superbest: Ora siamo decisamente alla fine "composizione non ereditarietà" dello spettro. Un razzo è composto da parti di razzi . Un razzo non è "un tipo speciale di elenco delle parti"! Ti suggerirei di tagliarlo ulteriormente; perché un elenco, al contrario di una IEnumerable<T>- una sequenza ?
Eric Lippert,

161

Wow, il tuo post ha un'intera serie di domande e punti. La maggior parte del ragionamento che ottieni da Microsoft è esattamente il punto. Cominciamo con tuttoList<T>

  • List<T> è altamente ottimizzato. Il suo utilizzo principale deve essere utilizzato come membro privato di un oggetto.
  • Microsoft non ha sigillarlo perché a volte si potrebbe desiderare di creare una classe che ha un nome più amichevole: class MyList<T, TX> : List<CustomObject<T, Something<TX>> { ... }. Ora è facile come fare var list = new MyList<int, string>();.
  • CA1002: Non esporre elenchi generici : in pratica, anche se hai intenzione di utilizzare questa app come unico sviluppatore, vale la pena svilupparsi con buone pratiche di codifica, in modo che si instillino in te e nella seconda natura. Puoi comunque esporre l'elenco come IList<T>se hai bisogno di un utente che abbia un elenco indicizzato. Questo ti consente di modificare l'implementazione all'interno di una classe in seguito.
  • Microsoft ha reso Collection<T>molto generico perché è un concetto generico ... il nome dice tutto; è solo una collezione. Ci sono versioni più precisi, come SortedCollection<T>, ObservableCollection<T>, ReadOnlyCollection<T>, ecc ognuno dei quali implementano IList<T>, ma non è List<T> .
  • Collection<T>consente ai membri (ad esempio Aggiungi, Rimuovi, ecc.) di essere sovrascritti perché sono virtuali. List<T>non.
  • L'ultima parte della tua domanda è precisa. Una squadra di calcio è più di un semplice elenco di giocatori, quindi dovrebbe essere una classe che contiene tale elenco di giocatori. Pensa alla composizione contro l'ereditarietà . Una squadra di calcio ha un elenco di giocatori (un elenco), non è un elenco di giocatori.

Se stavo scrivendo questo codice, la classe sarebbe probabilmente simile a questa:

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

6
"il tuo post ha un'intera serie di domande " - Beh, ho detto che ero completamente confuso. Grazie per il collegamento alla domanda di @ Readonly, ora vedo che gran parte della mia domanda è un caso speciale del più generale problema tra Composizione e Ereditarietà.
Superbo

3
@Brian: Tranne il fatto che non puoi creare un generico usando l'alias, tutti i tipi devono essere concreti. Nel mio esempio, List<T>viene esteso come una classe interna privata che utilizza generici per semplificare l'utilizzo e la dichiarazione di List<T>. Se l'estensione fosse semplicemente class FootballRoster : List<FootballPlayer> { }, allora sì avrei potuto usare un alias. Immagino che valga la pena notare che potresti aggiungere membri / funzionalità aggiuntivi, ma è limitato poiché non puoi ignorare membri importanti di List<T>.
mio

6
Se questo fosse Programmers.SE, sarei d'accordo con l'attuale gamma di voti di risposta, ma, poiché si tratta di un post SO, questa risposta tenta almeno di rispondere ai problemi specifici di C # (.NET), anche se ci sono determinati problemi di progettazione.
Mark Hurd,

7
Collection<T> allows for members (i.e. Add, Remove, etc.) to be overriddenOra che è un buon motivo per non ereditare dalla lista. Le altre risposte hanno troppa filosofia.
Navin,

10
@my: ho il sospetto che tu intenda "slew" di domande, dal momento che un investigatore è un detective.
combatti il

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

Il codice precedente significa: un gruppo di ragazzi della strada che giocano a calcio e hanno un nome. Qualcosa di simile a:

Persone che giocano a calcio

Comunque, questo codice (dalla mia risposta)

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

Mezzi: questa è una squadra di calcio che ha gestione, giocatori, amministratori, ecc. Qualcosa come:

Immagine che mostra i membri (e altri attributi) della squadra del Manchester United

Ecco come viene presentata la tua logica in immagini ...


9
Mi aspetto che il tuo secondo esempio sia una squadra di calcio, non una squadra di calcio. Una squadra di calcio ha manager e amministratori ecc. Da questa fonte : "Una squadra di calcio è il nome collettivo dato a un gruppo di giocatori ... Tali squadre potrebbero essere selezionate per giocare in una partita contro una squadra avversaria, per rappresentare una squadra di calcio ... ". Quindi penso che una squadra sia un elenco di giocatori (o forse più precisamente una raccolta di giocatori).
Ben

3
Più ottimizzatoprivate readonly List<T> _roster = new List<T>(44);
SiD

2
È necessario importare lo System.Linqspazio dei nomi.
Davide Cannizzo,

1
Per gli oggetti visivi: +1
V.7

115

Questo è un classico esempio di composizione vs eredità .

In questo caso specifico:

La squadra è un elenco di giocatori con un comportamento aggiuntivo

o

La squadra è un oggetto a sé stante che contiene un elenco di giocatori?

Estendendo l'elenco ti stai limitando in diversi modi:

  1. Non è possibile limitare l'accesso (ad esempio, interrompere le persone che cambiano il registro). Ottieni tutti i metodi dell'elenco, sia che tu ne abbia bisogno / che li desideri o meno.

  2. Cosa succede se vuoi avere anche elenchi di altre cose. Ad esempio, le squadre hanno allenatori, manager, tifosi, attrezzature, ecc. Alcuni di questi potrebbero essere elenchi a sé stanti.

  3. Limiti le tue opzioni per l'eredità. Ad esempio, potresti voler creare un oggetto Team generico e quindi avere BaseballTeam, FootballTeam, ecc. Che ereditano da quello. Per ereditare da Elenco devi fare l'eredità da Squadra, ma ciò significa che tutti i vari tipi di squadra sono costretti ad avere la stessa implementazione di quel roster.

Composizione - incluso un oggetto che dà il comportamento desiderato all'interno del tuo oggetto.

Ereditarietà: l'oggetto diventa un'istanza dell'oggetto con il comportamento desiderato.

Entrambi hanno i loro usi, ma questo è un caso chiaro in cui è preferibile la composizione.


1
Espandendosi su # 2, non ha senso che una Lista abbia una Lista.
Cruncher,

Innanzitutto, la domanda non ha nulla a che fare con la composizione rispetto all'eredità. La risposta è che OP non vuole implementare un tipo più specifico di elenco, quindi non dovrebbe estendere Elenco <>. Sono contorto perché hai punteggi molto alti su StackOverflow e dovresti saperlo chiaramente e la gente si fida di quello che dici, quindi ormai un minimo di 55 persone che hanno votato e la cui idea è confusa credono che un modo o l'altro sia ok per costruire un sistema ma chiaramente non lo è! # no-rage
Mauro Sampietro

6
@sam Vuole il comportamento dell'elenco. Ha due scelte, può estendere la lista (eredità) o può includere una lista all'interno del suo oggetto (composizione). Forse hai frainteso parte della domanda o della risposta piuttosto che 55 persone hanno torto e hai ragione? :)
Tim B

4
Sì. È un caso degenerato con un solo super e sub ma sto dando la spiegazione più generale per la sua domanda specifica. Può avere solo una squadra e quella può sempre avere una lista, ma la scelta è ancora quella di ereditare la lista o includerla come oggetto interno. Una volta che inizi a includere più tipi di squadra (calcio. Cricket. Ecc.) E iniziano a essere più di un semplice elenco di giocatori, vedi come hai la domanda completa sulla composizione rispetto all'eredità. Guardare il quadro generale è importante in casi come questo per evitare scelte sbagliate in anticipo che poi significano molto refactoring in seguito.
Tim B

3
L'OP non ha richiesto la composizione, ma il caso d'uso è un chiaro esempio del problema X: Y di cui uno dei 4 principi di programmazione orientata agli oggetti è rotto, usato in modo improprio o non completamente compreso. La risposta più chiara è scrivere una raccolta specializzata come una pila o una coda (che non sembra adattarsi al caso d'uso) o capire la composizione. Una squadra di calcio NON è un elenco di giocatori di calcio. Se l'OP lo abbia chiesto esplicitamente o meno è irrilevante, senza comprendere Astrazione, Incapsulamento, Ereditarietà e Polimorfismo, non capiranno la risposta, ergo, X: Y amok
Julia McGuigan,

67

Come tutti hanno sottolineato, una squadra di giocatori non è un elenco di giocatori. Questo errore è commesso da molte persone ovunque, forse a vari livelli di competenza. Spesso il problema è sottile e occasionalmente molto grave, come in questo caso. Tali progetti sono sbagliati perché violano il principio di sostituzione di Liskov . Internet ha molti buoni articoli che spiegano questo concetto, ad esempio http://en.wikipedia.org/wiki/Liskov_substitution_principle

In breve, ci sono due regole da conservare in una relazione padre / figlio tra le classi:

  • un bambino non dovrebbe richiedere caratteristiche inferiori a quelle che definiscono completamente il genitore.
  • un genitore non dovrebbe richiedere alcuna caratteristica in aggiunta a ciò che definisce completamente il figlio.

In altre parole, un genitore è una definizione necessaria di un figlio e un figlio è una definizione sufficiente di un genitore.

Ecco un modo di pensare attraverso la soluzione e applicare il principio di cui sopra che dovrebbe aiutare a evitare un tale errore. Si dovrebbero verificare le proprie ipotesi verificando se tutte le operazioni di una classe genitore sono valide per la classe derivata sia strutturalmente che semanticamente.

  • Una squadra di calcio è un elenco di giocatori di calcio? (Tutte le proprietà di un elenco si applicano a una squadra con lo stesso significato)
    • Una squadra è una raccolta di entità omogenee? Sì, la squadra è una raccolta di giocatori
    • L'ordine di inclusione dei giocatori è descrittivo dello stato della squadra e la squadra assicura che la sequenza venga preservata se non viene esplicitamente modificata? No e No
    • I giocatori dovrebbero essere inclusi / eliminati in base alla loro posizione sequenziale nella squadra? No

Come vedi, solo la prima caratteristica di un elenco è applicabile a una squadra. Quindi una squadra non è un elenco. Un elenco sarebbe un dettaglio di implementazione di come gestisci la tua squadra, quindi dovrebbe essere usato solo per conservare gli oggetti del giocatore e essere manipolato con metodi di classe Team.

A questo punto, vorrei sottolineare che una classe del Team, secondo me, non dovrebbe nemmeno essere implementata usando un Elenco; nella maggior parte dei casi dovrebbe essere implementato utilizzando una struttura di dati Set (HashSet, ad esempio).


8
Bello catturare la lista contro Set. Sembra un errore fatto troppo spesso. Quando in una raccolta può esserci solo un'istanza di un elemento, una sorta di set o dizionario dovrebbe essere un candidato preferito per l'implementazione. Va bene avere una squadra con due giocatori con lo stesso nome. Non è giusto avere un giocatore incluso due volte contemporaneamente.
Fred Mitchell,

1
+1 per alcuni punti positivi, sebbene sia più importante chiedere "può una struttura di dati supportare ragionevolmente tutto ciò che è utile (idealmente a breve e lungo termine)", piuttosto che "la struttura dei dati fa più di quanto sia utile".
Tony Delroy,

1
@TonyD Bene, i punti che sto sollevando non è che si dovrebbe verificare "se la struttura dei dati fa più di quanto sia utile". È per verificare "Se la struttura di dati Parent fa qualcosa di irrilevante, insignificante o contro-intuitivo al comportamento che la classe Child potrebbe implicare".
Satyan Raina,

1
@TonyD In realtà c'è un problema con le caratteristiche irrilevanti derivate da un genitore in quanto fallirebbe i test negativi in ​​molti casi. Un programmatore potrebbe estendere Human {eat (); correre(); write ();} da un Gorilla {eat (); correre(); swing ();} pensando che non c'è niente di sbagliato in un essere umano con una caratteristica in più di poter oscillare. E poi in un mondo di gioco il tuo umano inizia improvvisamente a bypassare tutti i presunti ostacoli terrestri semplicemente oscillando sugli alberi. A meno che non sia esplicitamente specificato, un Umano pratico non dovrebbe essere in grado di oscillare. Un tale design lascia l'api molto aperta agli abusi e alla confusione
Satyan Raina,

1
@TonyD Non sto suggerendo che la classe Player debba essere derivata da HashSet. Sto suggerendo che la classe Player dovrebbe "nella maggior parte dei casi" essere implementata usando un HashSet tramite Composizione e che sia totalmente un dettaglio a livello di implementazione, non a livello di progettazione (ecco perché l'ho menzionato come un sidenote alla mia risposta). Potrebbe benissimo essere implementato usando un elenco se esiste una giustificazione valida per tale implementazione. Quindi, per rispondere alla tua domanda, è necessario avere la ricerca O (1) per chiave? No. Pertanto NON si dovrebbe estendere un giocatore anche da un HashSet.
Satyan Raina,

50

Che cosa succede se la FootballTeamsquadra ha delle riserve insieme alla squadra principale?

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

Come lo modelleresti con?

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

La relazione è chiaramente ha un e non è un .

o RetiredPlayers?

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

Come regola generale, se mai desideri ereditare da una raccolta, dai un nome alla classe SomethingCollection.

Il vostro SomethingCollectionsemanticamente ha senso? Fallo solo se il tuo tipo è una raccolta di Something.

Nel caso FootballTeamnon suona bene. A Teamè più di a Collection. A Teampuò avere allenatori, formatori, ecc. Come hanno indicato le altre risposte.

FootballCollectionsembra una raccolta di palloni da calcio o forse una raccolta di accessori per il calcio. TeamCollection, una raccolta di squadre.

FootballPlayerCollectionsuona come una raccolta di giocatori che sarebbe un nome valido per una classe che eredita List<FootballPlayer>se davvero volessi farlo.

È davvero List<FootballPlayer>un tipo perfettamente buono da affrontare. Forse IList<FootballPlayer>se lo stai restituendo da un metodo.

In sintesi

Chiedilo a te stesso

  1. È X un Y? o ha X un Y?

  2. I nomi delle mie classi significano cosa sono?


Se il tipo di ogni giocatore sarebbe classificare come appartenente a uno DefensivePlayers, OffensivePlayerso OtherPlayers, potrebbe essere legittimamente utile avere un tipo che potrebbe essere utilizzato da codice che si aspetta un List<Player>ma i membri inclusi anche DefensivePlayers, OffsensivePlayerso SpecialPlayersdi tipo IList<DefensivePlayer>, IList<OffensivePlayer>e IList<Player>. Si potrebbe usare un oggetto separato per memorizzare nella cache gli elenchi separati, ma incapsularli nello stesso oggetto dell'elenco principale sembrerebbe più pulito [usare l'invalidazione di un elenco di elenchi ...
supercat

... come spunto per il fatto che l'elenco principale è cambiato e gli elenchi secondari dovranno essere rigenerati al prossimo accesso].
Supercat,

2
Mentre sono d'accordo con il punto sollevato, mi strappa davvero l'anima nel vedere qualcuno dare consigli di progettazione e suggerire di esporre una Lista concreta <T> con un getter pubblico e un setter in un oggetto business :(
sara,

38

Design> Implementazione

Quali metodi e proprietà si espongono è una decisione di progettazione. La classe di base da cui erediti è un dettaglio di implementazione. Sento che vale la pena fare un passo indietro verso il primo.

Un oggetto è una raccolta di dati e comportamenti.

Quindi le tue prime domande dovrebbero essere:

  • Quali dati comprende questo oggetto nel modello che sto creando?
  • Quale comportamento presenta questo oggetto in quel modello?
  • Come potrebbe cambiare questo in futuro?

Tieni presente che l'eredità implica una relazione "isa" (è una), mentre la composizione implica una relazione "ha una" (hasa). Scegli quello giusto per la tua situazione a tuo avviso, tenendo presente dove potrebbero andare le cose mentre la tua applicazione si evolve.

Considera di pensare nelle interfacce prima di pensare in tipi concreti, poiché alcune persone trovano più facile mettere il loro cervello in "modalità di progettazione" in quel modo.

Questo non è qualcosa che tutti fanno consapevolmente a questo livello nella codifica quotidiana. Ma se stai meditando questo tipo di argomento, stai camminando in acque di design. Essere consapevoli di ciò può essere liberatorio.

Prendi in considerazione le specifiche di progettazione

Dai un'occhiata a Elenco <T> e IList <T> su MSDN o Visual Studio. Scopri quali metodi e proprietà espongono. Questi metodi sembrano tutti qualcosa che qualcuno vorrebbe fare a FootballTeam secondo te?

FootballTeam.Reverse () ha senso per te? FootballTeam.ConvertAll <TOutput> () sembra qualcosa che desideri?

Questa non è una domanda trabocchetto; la risposta potrebbe essere davvero "sì". Se implementi / erediti List <Player> o IList <Player>, sei bloccato con loro; se è l'ideale per il tuo modello, fallo.

Se decidi di sì, questo ha senso, e vuoi che il tuo oggetto sia curabile come una raccolta / lista di giocatori (comportamento), e quindi vuoi implementare ICollection o IList, in ogni caso fallo. fittiziamente:

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

Se vuoi che il tuo oggetto contenga una raccolta / lista di giocatori (dati), e quindi vuoi che la raccolta o la lista siano una proprietà o un membro, fallo in ogni caso. fittiziamente:

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

Potresti sentire che vuoi che le persone siano in grado di enumerare solo l'insieme di giocatori, piuttosto che contarli, aggiungerli o rimuoverli. IEnumerable <Player> è un'opzione perfettamente valida da considerare.

Potresti pensare che nessuna di queste interfacce sia utile nel tuo modello. Questo è meno probabile (IEnumerable <T> è utile in molte situazioni) ma è ancora possibile.

Chiunque tenti di dirti che uno di questi è categoricamente e definitivamente sbagliato in ogni caso è fuorviato. Chiunque tenti di dirti che è categoricamente e definitivamente giusto in ogni caso è fuorviato.

Passa all'implementazione

Dopo aver deciso su dati e comportamento, puoi prendere una decisione sull'implementazione. Ciò include da quali classi concrete si dipende tramite ereditarietà o composizione.

Questo potrebbe non essere un grande passo e le persone spesso confondono il design e l'implementazione poiché è del tutto possibile scorrere tutto in testa in un secondo o due e iniziare a scrivere via.

Un esperimento di pensiero

Un esempio artificiale: come altri hanno già detto, una squadra non è sempre "solo" una raccolta di giocatori. Mantenete una raccolta di punteggi delle partite per la squadra? La squadra è intercambiabile con il club, nel tuo modello? Se è così, e se la tua squadra è una raccolta di giocatori, forse è anche una raccolta di personale e / o una raccolta di punteggi. Quindi finisci con:

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

Nonostante la progettazione, a questo punto in C # non sarai in grado di implementarli tutti ereditando comunque dall'elenco <T>, poiché C # "solo" supporta l'ereditarietà singola. (Se hai provato questo maleficio in C ++, potresti considerarlo una buona cosa.) L'implementazione di una raccolta tramite ereditarietà e una tramite composizione rischia di sembrare sporca. E proprietà come Count diventano confuse per gli utenti a meno che non si implementi esplicitamente ILIst <Player> .Count e IList <StaffMember> .Count ecc., E quindi sono solo dolorose piuttosto che confuse. Puoi vedere dove sta andando; sentirsi bene mentre si pensa a questa strada potrebbe anche dirti che è sbagliato dirigersi in questa direzione (e giustamente o erroneamente, i tuoi colleghi potrebbero anche se lo avessi implementato in questo modo!)

La risposta breve (troppo tardi)

La linea guida per non ereditare dalle classi di raccolta non è specifica per C #, la troverai in molti linguaggi di programmazione. Si riceve la saggezza non una legge. Uno dei motivi è che nella pratica la composizione è considerata spesso vincente per eredità in termini di comprensibilità, attuabilità e manutenibilità. È più comune con gli oggetti del mondo reale / dominio trovare relazioni "hasa" utili e coerenti rispetto alle relazioni "isa" utili e coerenti a meno che tu non sia nel profondo dell'astratto, soprattutto con il passare del tempo e con i dati precisi e il comportamento degli oggetti nel codice i cambiamenti. Questo non dovrebbe farti escludere sempre l'eredità dalle classi di raccolta; ma può essere suggestivo.


34

Prima di tutto, ha a che fare con l'usabilità. Se usi l'ereditarietà, la Teamclasse esporrà comportamenti (metodi) progettati esclusivamente per la manipolazione di oggetti. Ad esempio, AsReadOnly()oppure i CopyTo(obj)metodi non hanno senso per l'oggetto team. Invece del AddRange(items)metodo probabilmente vorrai un AddPlayers(players)metodo più descrittivo .

Se si desidera utilizzare LINQ, l'implementazione di un'interfaccia generica come ICollection<T>o IEnumerable<T>avrebbe più senso.

Come accennato, la composizione è il modo giusto di procedere. Basta implementare un elenco di giocatori come variabile privata.


30

Una squadra di calcio non è un elenco di giocatori di calcio. Una squadra di calcio è composta da un elenco di giocatori di calcio!

Questo è logicamente sbagliato:

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

e questo è corretto:

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

19
Questo non spiega perché . L'OP sa che la maggior parte degli altri programmatori la pensa così, semplicemente non capisce perché sia ​​importante. Questo sta davvero fornendo informazioni già nella domanda.
Serve l'

2
Questa è la prima cosa a cui ho pensato, come i suoi dati sono stati modellati in modo errato. Quindi il motivo è che il tuo modello di dati non è corretto.
rball,

3
Perché non ereditare dall'elenco? Perché non vuole un tipo più specifico di elenco.
Mauro Sampietro,

2
Non penso che il secondo esempio sia corretto. Stai esponendo lo stato e stai quindi rompendo l'incapsulamento. Lo stato dovrebbe essere nascosto / interno all'oggetto e il modo di mutare questo stato dovrebbe essere tramite metodi pubblici. Esattamente quali metodi è qualcosa da determinare in base ai requisiti aziendali, ma dovrebbe essere basato su un "bisogno di questo adesso", non "potrebbe essere utile qualche volta non lo so". Tieni la tua api stretta e risparmierai tempo e fatica.
Sara,

1
L'incapsulamento non è il punto della domanda. Mi concentro sul non estendere un tipo se non vuoi che quel tipo si comporti in un modo più specifico. Quei campi dovrebbero essere autoproperties in c # e ottenere / impostare metodi in altre lingue, ma qui in questo contesto è del tutto superfluo
Mauro Sampietro,

28

Dipende dal contesto

Quando consideri la tua squadra come un elenco di giocatori, stai proiettando l '"idea" di una squadra di foot ball su un aspetto: riduci la "squadra" alle persone che vedi sul campo. Questa proiezione è corretta solo in un determinato contesto. In un contesto diverso, questo potrebbe essere completamente sbagliato. Immagina di voler diventare uno sponsor della squadra. Quindi devi parlare con i dirigenti della squadra. In questo contesto, il team viene proiettato nell'elenco dei suoi manager. E questi due elenchi di solito non si sovrappongono molto. Altri contesti sono quelli attuali rispetto a quelli precedenti, ecc.

Semantica poco chiara

Quindi il problema nel considerare una squadra come un elenco dei suoi giocatori è che la sua semantica dipende dal contesto e che non può essere estesa quando il contesto cambia. Inoltre, è difficile esprimere quale contesto si sta utilizzando.

Le lezioni sono estensibili

Quando si utilizza una classe con un solo membro (ad esempio IList activePlayers), è possibile utilizzare il nome del membro (e inoltre il suo commento) per chiarire il contesto. Quando sono presenti contesti aggiuntivi, è sufficiente aggiungere un membro aggiuntivo.

Le lezioni sono più complesse

In alcuni casi potrebbe essere eccessivo creare una classe aggiuntiva. Ogni definizione di classe deve essere caricata tramite il classloader e verrà memorizzata nella cache dalla macchina virtuale. Questo ti costa prestazioni di runtime e memoria. Quando hai un contesto molto specifico, potrebbe essere OK considerare una squadra di calcio come un elenco di giocatori. Ma in questo caso, dovresti semplicemente usare una IList , non una classe derivata da essa.

Conclusione / Considerazioni

Quando hai un contesto molto specifico, è OK considerare una squadra come un elenco di giocatori. Ad esempio all'interno di un metodo è del tutto OK scrivere:

IList<Player> footballTeam = ...

Quando si utilizza F #, può anche essere OK creare un'abbreviazione di tipo:

type FootballTeam = IList<Player>

Ma quando il contesto è più ampio o addirittura poco chiaro, non dovresti farlo. Questo è particolarmente vero quando si crea una nuova classe il cui contesto in cui potrebbe essere utilizzato in futuro non è chiaro. Un segnale di avvertimento è quando inizi ad aggiungere ulteriori attributi alla tua classe (nome della squadra, allenatore, ecc.). Questo è un chiaro segno che il contesto in cui verrà utilizzata la classe non è fisso e cambierà in futuro. In questo caso non puoi considerare la squadra come un elenco di giocatori, ma dovresti modellare la lista dei giocatori (attualmente attivi, non infortunati, ecc.) Come un attributo della squadra.


27

Vorrei riscrivere la tua domanda. quindi potresti vedere l'argomento da una prospettiva diversa.

Quando devo rappresentare una squadra di calcio, capisco che è fondamentalmente un nome. Tipo: "Le aquile"

string team = new string();

Poi ho capito che anche le squadre hanno giocatori.

Perché non posso semplicemente estendere il tipo di stringa in modo che contenga anche un elenco di giocatori?

Il tuo punto di accesso al problema è arbitrario. Provate a pensare che cosa fa una squadra ha (proprietà), non è quello che è .

Dopo averlo fatto, potresti vedere se condivide le proprietà con altre classi. E pensa all'eredità.


1
Questo è un buon punto: si potrebbe pensare alla squadra solo come un nome. Tuttavia, se la mia applicazione mira a lavorare con le azioni dei giocatori, quel pensiero è un po 'meno ovvio. Ad ogni modo, il problema sembra dipendere dalla composizione contro l'eredità alla fine.
Superbo

6
Questo è un modo meritorio di vederlo. Come nota a margine, ti preghiamo di considerare questo: un uomo ricco possiede diverse squadre di calcio, lo regala a un amico, l'amico cambia il nome della squadra, licenzia l'allenatore, e sostituisce tutti i giocatori. La squadra di amici incontra la squadra maschile sul green e mentre la squadra maschile sta perdendo, dice "Non posso credere che tu mi stia battendo con la squadra che ti ho dato!" l'uomo ha ragione? come lo verificheresti?
user1852503,

20

Solo perché penso che le altre risposte vadano in giro a capire se una squadra di calcio "è-a" List<FootballPlayer>o "ha-a" List<FootballPlayer>, che in realtà non risponde a questa domanda come scritto.

Il PO chiede principalmente chiarimenti sugli orientamenti per l'ereditarietà di List<T>:

Una linea guida dice che non dovresti ereditare List<T>. Perchè no?

Perché List<T>non ha metodi virtuali. Questo è meno un problema nel tuo codice, dal momento che di solito puoi cambiare l'implementazione con relativamente poca sofferenza, ma può essere un affare molto più grande in un'API pubblica.

Che cos'è un'API pubblica e perché dovrei preoccuparmi?

Un'API pubblica è un'interfaccia che esponi a programmatori di terze parti. Pensa al codice quadro. E ricorda che le linee guida a cui si fa riferimento sono le " Linee guida per la progettazione di .NET Framework " e non le " Linee guida per la progettazione di applicazioni .NET ". C'è una differenza e, in generale, la progettazione di API pubbliche è molto più rigorosa.

Se il mio progetto attuale non ha e non è probabile che abbia questa API pubblica, posso tranquillamente ignorare questa linea guida? Se eredito da List e risulta che ho bisogno di un'API pubblica, quali difficoltà dovrò avere?

Praticamente, si. Potresti voler considerare la logica alla base per vedere se si applica comunque alla tua situazione, ma se non stai costruendo un'API pubblica, non devi preoccuparti in particolare delle preoccupazioni relative all'API come il controllo delle versioni (di cui, si tratta di sottoinsieme).

Se in futuro aggiungi un'API pubblica, dovrai estrarre l'API dalla tua implementazione (non esponendola List<T>direttamente) o violare le linee guida con l'eventuale futuro dolore che ciò comporta.

Perché è ancora importante? Un elenco è un elenco. Cosa potrebbe cambiare? Cosa potrei voler cambiare?

Dipende dal contesto, ma dal momento che stiamo usando FootballTeamcome esempio - immagina di non poter aggiungere un FootballPlayerse ciò causerebbe il superamento del limite salariale da parte della squadra. Un possibile modo di aggiungere sarebbe qualcosa del tipo:

 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 ... ma non puoi override Addperché non lo è virtual(per motivi di performance).

Se ti trovi in ​​un'applicazione (il che, fondamentalmente, significa che tu e tutti i tuoi chiamanti siete compilati insieme) ora potete cambiare usando IList<T>e correggere eventuali errori di compilazione:

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

ma, se sei stato esposto pubblicamente a una terza parte, hai appena apportato una modifica sostanziale che causerà errori di compilazione e / o di runtime.

TL; DR: le linee guida sono per le API pubbliche. Per le API private, fai quello che vuoi.


18

Permettere alle persone di dire

myTeam.subList(3, 5);

ha un senso a tutti? In caso contrario, non dovrebbe essere un elenco.


3
Potrebbe se lo chiamimyTeam.subTeam(3, 5);
Sam Leach,

3
@SamLeach Supponendo che sia vero, allora avrai comunque bisogno di composizione e non ereditarietà. Come subListnon tornerà Teampiù nemmeno a .
Cruncher,

1
Questo mi ha convinto più di ogni altra risposta. +1
FireCubez

16

Dipende dal comportamento dell'oggetto "team". Se si comporta proprio come una raccolta, potrebbe essere OK rappresentarlo prima con un semplice Elenco. Quindi potresti iniziare a notare che continui a duplicare il codice che scorre nell'elenco; a questo punto hai la possibilità di creare un oggetto FootballTeam che avvolge l'elenco dei giocatori. La classe FootballTeam diventa la sede di tutto il codice che scorre nell'elenco dei giocatori.

Rende inutilmente prolisso il mio codice. Ora devo chiamare my_team.Players.Count invece che solo my_team.Count. Per fortuna, con C # posso definire gli indicizzatori per rendere trasparente l'indicizzazione e inoltrare tutti i metodi dell'elenco interno ... Ma questo è un sacco di codice! Cosa ottengo per tutto quel lavoro?

Incapsulamento. I tuoi clienti non devono sapere cosa succede all'interno di FootballTeam. Per quanto sanno tutti i tuoi clienti, potrebbe essere implementato cercando l'elenco dei giocatori in un database. Non hanno bisogno di sapere, e questo migliora il tuo design.

Semplicemente non ha alcun senso. Una squadra di calcio non "ha" un elenco di giocatori. È la lista dei giocatori. Non dici "John McFootballer si è unito ai giocatori di SomeTeam". Dici "John è entrato in SomeTeam". Non aggiungi una lettera ai "caratteri di una stringa", aggiungi una lettera a una stringa. Non aggiungi un libro ai libri di una biblioteca, aggiungi un libro a una biblioteca.

Esatto :) dirai footballTeam.Add (john), non footballTeam.List.Add (john). L'elenco interno non sarà visibile.


1
OP vuole capire come MODELLARE LA REALTÀ ma poi definisce una squadra di calcio come un elenco di giocatori di calcio (che è sbagliato concettualmente). Questo è il problema. A mio avviso, qualsiasi altra argomentazione gli è fuorviante.
Mauro Sampietro,

3
Non sono d'accordo sul fatto che l'elenco dei giocatori sia sbagliato concettualmente. Non necessariamente. Come qualcun altro ha scritto in questa pagina, "Tutti i modelli sono sbagliati, ma alcuni sono utili"
xpmatteo,

I modelli giusti sono difficili da ottenere, una volta fatti sono quelli utili. Se un modello può essere utilizzato in modo improprio, è concettualmente sbagliato. stackoverflow.com/a/21706411/711061
Mauro Sampietro il

15

Ci sono molte risposte eccellenti qui, ma voglio toccare qualcosa che non ho visto menzionato: il design orientato agli oggetti riguarda il potenziamento degli oggetti .

Vuoi incapsulare tutte le tue regole, lavoro aggiuntivo e dettagli interni all'interno di un oggetto appropriato. In questo modo altri oggetti che interagiscono con questo non devono preoccuparsi di tutto. In effetti, vuoi fare un passo ulteriore e impedire attivamente ad altri oggetti di bypassare questi interni.

Quando erediti da List, tutti gli altri oggetti possono vederti come un Elenco. Hanno accesso diretto ai metodi per aggiungere e rimuovere giocatori. E avrai perso il controllo; per esempio:

Supponiamo che tu voglia differenziare quando un giocatore lascia sapendo se si sono ritirati, si sono dimessi o sono stati licenziati. È possibile implementare un RemovePlayermetodo che accetta un'enumerazione di input appropriata. Tuttavia, ereditando da List, si sarebbe in grado di impedire l'accesso diretto a Remove, RemoveAlle persino Clear. Di conseguenza, hai effettivamente impotenti tua FootballTeamclasse.


Ulteriori pensieri sull'incapsulamento ... Hai sollevato la seguente preoccupazione:

Rende inutilmente prolisso il mio codice. Ora devo chiamare my_team.Players.Count invece che solo my_team.Count.

Hai ragione, sarebbe inutilmente prolisso per tutti i clienti usare la tua squadra. Tuttavia, questo problema è molto piccolo in confronto al fatto che sei stato esposto List Playersa tutti in modo che possano giocherellare con la tua squadra senza il tuo consenso.

Continua dicendo:

Semplicemente non ha alcun senso. Una squadra di calcio non "ha" un elenco di giocatori. È la lista dei giocatori. Non dici "John McFootballer si è unito ai giocatori di SomeTeam". Dici "John è entrato in SomeTeam".

Ti sbagli sul primo bit: elimina la parola "elenco", ed è in realtà evidente che una squadra ha giocatori.
Tuttavia, hai colpito l'unghia sulla testa con il secondo. Non vuoi che i clienti chiamino ateam.Players.Add(...). Li vuoi chiamare ateam.AddPlayer(...). E la tua implementazione chiamerebbe (probabilmente tra l'altro) Players.Add(...)internamente.


Spero che tu possa vedere quanto sia importante l'incapsulamento per l'obiettivo di potenziare i tuoi oggetti. Vuoi consentire ad ogni classe di fare bene il suo lavoro senza paura di interferenze da altri oggetti.


12

Qual è il modo corretto di rappresentare una struttura di dati in C # ...

Ricorda, "Tutti i modelli sono sbagliati, ma alcuni sono utili." - Scatola George EP

Non esiste un "modo corretto", solo un modo utile.

Scegli quello che è utile a te e / ai tuoi utenti. Questo è tutto. Sviluppa economicamente, non sovraccaricare. Meno codice scrivi, meno codice dovrai eseguire il debug. (leggi le seguenti edizioni).

-- Modificato

La mia migliore risposta sarebbe ... dipende. Ereditare da un elenco esponerebbe i clienti di questa classe a metodi che potrebbero non essere esposti, principalmente perché FootballTeam assomiglia a un'entità aziendale.

- Edizione 2

Sinceramente non ricordo a cosa mi stavo riferendo nel commento "non sovraccaricare". Mentre credo che la mentalità dei KISS sia una buona guida, voglio sottolineare che ereditare una business class da List creerebbe più problemi di quanti ne risolva, a causa delle perdite di astrazione .

D'altra parte, credo che ci siano un numero limitato di casi in cui è utile semplicemente ereditare da Elenco. Come ho scritto nell'edizione precedente, dipende. La risposta a ciascun caso è fortemente influenzata dalla conoscenza, dall'esperienza e dalle preferenze personali.

Grazie a @kai per avermi aiutato a pensare più precisamente alla risposta.


La mia migliore risposta sarebbe ... dipende. Ereditare da un elenco esponerebbe i clienti di questa classe a metodi che potrebbero non essere esposti, principalmente perché FootballTeam assomiglia a un'entità aziendale. Non so se dovrei modificare questa risposta o pubblicarne un'altra, qualche idea?
ArturoTena,

2
Il fatto di " ereditare da un Listesporre i client di questa classe a metodi che potrebbero non essere esposti " è precisamente ciò che rende ereditare da Listun no-no assoluto. Una volta esposti, vengono gradualmente abusati e maltrattati nel tempo. Risparmiare tempo "sviluppando economicamente" può facilmente portare a dieci volte i risparmi persi in futuro: il debug degli abusi e infine il refactoring per correggere l'eredità. Il principio YAGNI può anche essere pensato come un significato: non hai bisogno di tutti quei metodi da List, quindi non esporli.
Disilluso il

3
"non sovraccaricare. Meno codice scrivi, meno codice dovrai eseguire il debug." <- Penso che questo sia fuorviante. L'incapsulamento e la composizione sull'ereditarietà NON sono un'ingegneria eccessiva e non causano più necessità di debug. Incapsulando stai LIMITANDO il numero di modi in cui i clienti possono usare (e abusare) della tua classe e quindi hai meno punti di accesso che richiedono test e input di convalida. Ereditare da Listperché è rapido e semplice e quindi porterebbe a un minor numero di bug è semplicemente sbagliato, è solo una cattiva progettazione e una cattiva progettazione porta a molti più bug di "over engineering".
Sara,

@kai sono d'accordo con te in ogni punto. Sinceramente non ricordo a cosa mi stavo riferendo nel commento "non sovraccaricare". OTOH, credo che ci siano un numero limitato di casi in cui è utile semplicemente ereditare da List. Come ho scritto nella versione successiva, dipende. La risposta a ciascun caso è fortemente influenzata dalla conoscenza, dall'esperienza e dalle preferenze personali. Come tutto nella vita. ;-)
ArturoTena,

11

Questo mi ricorda che "È un" contro "ha un" compromesso. A volte è più facile e sensato ereditare direttamente da una super classe. Altre volte ha più senso creare una classe autonoma e includere la classe da cui avresti ereditato come variabile membro. È ancora possibile accedere alla funzionalità della classe ma non sono associati all'interfaccia o ad altri vincoli che potrebbero derivare dall'ereditarietà della classe.

Tu che fai? Come per molte cose ... dipende dal contesto. La guida che vorrei usare è che per ereditare da un'altra classe dovrebbe esserci davvero una relazione "è un". Quindi, se stai scrivendo una classe chiamata BMW, potrebbe ereditare da Auto perché una BMW è davvero un'auto. Una classe Cavallo può ereditare dalla classe Mammifero perché un cavallo è in realtà un mammifero nella vita reale e qualsiasi funzionalità Mammifero dovrebbe essere rilevante per Cavallo. Ma puoi dire che una squadra è un elenco? Da quello che posso dire, non sembra che una squadra sia "una" lista. Quindi, in questo caso, avrei un elenco come variabile membro.


9

Il mio sporco segreto: non mi interessa cosa dicono le persone, e lo faccio. .NET Framework viene diffuso con "XxxxCollection" (UIElementCollection per l'esempio top of my head).

Quindi cosa mi impedisce di dire:

team.Players.ByName("Nicolas")

Quando lo trovo migliore di

team.ByName("Nicolas")

Inoltre, il mio PlayerCollection potrebbe essere utilizzato da altre classi, come "Club" senza duplicazione del codice.

club.Players.ByName("Nicolas")

Le migliori pratiche di ieri potrebbero non essere quelle di domani. Non vi è alcun motivo alla base della maggior parte delle migliori pratiche, la maggior parte è solo un ampio consenso tra la comunità. Invece di chiedere alla comunità se ti incolperà quando lo fai, cosa è più leggibile e mantenibile?

team.Players.ByName("Nicolas") 

o

team.ByName("Nicolas")

Veramente. Hai qualche dubbio? Ora forse hai bisogno di giocare con altri vincoli tecnici che ti impediscono di usare List <T> nel tuo caso d'uso reale. Ma non aggiungere un vincolo che non dovrebbe esistere. Se Microsoft non ha documentato il perché, allora è sicuramente una "best practice" proveniente dal nulla.


9
Mentre è importante avere il coraggio e l'iniziativa per sfidare la saggezza accettata quando appropriato, penso che sia saggio capire prima perché la saggezza accettata è stata accettata in primo luogo, prima di imbarcarsi per sfidarla. -1 perché "Ignora quella linea guida!" non è una buona risposta a "Perché esiste questa linea guida?" Per inciso, la comunità non mi ha solo "incolpato" in questo caso, ma ha fornito una spiegazione soddisfacente che è riuscita a persuadermi.
Superbo

3
In realtà, quando non viene fatta alcuna spiegazione, il tuo unico modo è spingere il confine e testare non aver paura dell'infrazione. Adesso. Chiediti, anche se Lista <T> non è stata una buona idea. Quanto sarebbe difficile cambiare l'eredità da Elenco <T> a Collezione <T>? Un'ipotesi: meno tempo rispetto alla pubblicazione sul forum, qualunque sia la lunghezza del tuo codice con strumenti di refactoring. Ora è una buona domanda, ma non pratica.
Nicolas Dorier,

Per chiarire: non sto certamente aspettando la benedizione di SO per ereditare da tutto ciò che voglio, comunque lo voglio, ma il punto della mia domanda era capire le considerazioni che dovrebbero essere prese in questa decisione e perché così tanti programmatori esperti sembrano avere decise il contrario di quello che feci. Collection<T>non è la stessa cosa, List<T>quindi potrebbe essere necessario un po 'di lavoro per verificare in un progetto di grandi dimensioni.
Superbo

2
team.ByName("Nicolas")significa "Nicolas"è il nome della squadra.
Sam Leach,

1
"non aggiungere un vincolo che non dovrebbe esistere" Au contraire, non esporre nulla che non hai una buona ragione per esporre. Questo non è purismo o fervore cieco, né è l'ultima tendenza della moda del design. Si tratta di un design orientato agli oggetti di base 101, livello principiante. Non è un segreto il motivo per cui esiste questa "regola", è il risultato di diversi decenni di esperienza.
Sara,

8

Ciò che le linee guida dicono è che l'API pubblica non dovrebbe rivelare la decisione di progettazione interna se si sta utilizzando un elenco, un set, un dizionario, un albero o altro. Una "squadra" non è necessariamente un elenco. È possibile implementarlo come un elenco, ma gli utenti dell'API pubblica dovrebbero utilizzare la classe in base alle necessità. Ciò ti consente di modificare la tua decisione e utilizzare una diversa struttura di dati senza influire sull'interfaccia pubblica.


In retrospettiva, dopo la spiegazione di @EricLippert e altri, in realtà hai dato un'ottima risposta per la parte API della mia domanda - oltre a quello che hai detto, se lo faccio class FootballTeam : List<FootballPlayers>, gli utenti della mia classe saranno in grado di dire che ' ve ereditato da List<T>utilizzando la riflessione, vedendo i List<T>metodi che non hanno senso per una squadra di calcio, e di essere in grado di utilizzare FootballTeamin List<T>, quindi vorrei essere rivelatrici dettagli di implementazione per il cliente (inutilmente).
Superbo

7

Quando dicono che List<T>è "ottimizzato" penso che vogliano dire che non ha caratteristiche come i metodi virtuali che sono un po 'più costose. Quindi il problema è che una volta che si espone List<T>in pubblico API , si perde la capacità di far rispettare le regole di business o di personalizzare la sua funzionalità in seguito. Ma se stai usando questa classe ereditata come interna al tuo progetto (al contrario di potenzialmente esposta a migliaia di tuoi clienti / partner / altri team come API), allora potrebbe essere OK se ti fa risparmiare tempo ed è la funzionalità che vuoi duplicare. Il vantaggio di ereditare da List<T>è che si elimina molto codice stupido wrapper che non verrà mai personalizzato in un futuro prevedibile. Anche se vuoi che la tua classe abbia esplicitamente la stessa semantica diList<T> per la durata delle tue API, allora potrebbe anche essere OK.

Vedo spesso molte persone che fanno tonnellate di lavoro extra solo a causa della regola di FxCop, o il blog di qualcuno dice che è una pratica "cattiva". Molte volte, questo trasforma il codice in un modello di stranezza palooza. Come per molte linee guida, trattale come linea guida che può avere eccezioni.


4

Anche se non ho un confronto complesso come la maggior parte di queste risposte, vorrei condividere il mio metodo per gestire questa situazione. Estendendo IEnumerable<T>, puoi consentire alla tua Teamclasse di supportare le estensioni di query Linq, senza esporre pubblicamente tutti i metodi e le proprietà di 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
{
    ...
}

3

Se gli utenti della tua classe hanno bisogno di tutti i metodi e le proprietà dell'elenco **, dovresti derivarne la classe. Se non ne hanno bisogno, racchiudi l'Elenco e crea involucri per i metodi di cui gli utenti della classe hanno effettivamente bisogno.

Questa è una regola rigorosa, se scrivi un'API pubblica o qualsiasi altro codice che verrà utilizzato da molte persone. Puoi ignorare questa regola se hai una piccola app e non più di 2 sviluppatori. Questo ti farà risparmiare un po 'di tempo.

Per le app di piccole dimensioni, puoi anche prendere in considerazione la scelta di un altro linguaggio meno rigoroso. Ruby, JavaScript: tutto ciò che ti consente di scrivere meno codice.


3

Volevo solo aggiungere che Bertrand Meyer, l'inventore di Eiffel e il design per contratto, avrebbero Teamereditato List<Player>senza battere ciglio.

Nel suo libro, Object-Oriented Software Construction , discute l'implementazione di un sistema GUI in cui le finestre rettangolari possono avere finestre figlio. Ha semplicemente Windowereditato da entrambi Rectanglee Tree<Window>riutilizzare l'implementazione.

Tuttavia, C # non è Eiffel. Quest'ultimo supporta l'ereditarietà multipla e la ridenominazione delle funzionalità . In C #, quando esegui la sottoclasse, erediti sia l'interfaccia che l'implementazione. È possibile ignorare l'implementazione, ma le convenzioni di chiamata vengono copiate direttamente dalla superclasse. In Eiffel, tuttavia, è possibile modificare i nomi dei metodi pubblici, in modo da poter rinominare Adde Removenel Hiree Firenel proprio Team. Se un'istanza di Teamviene reimpostata su List<Player>, il chiamante la utilizzerà Adde Removeper modificarla, ma i metodi virtuali Hiree Fireverranno chiamati.


1
Qui il problema non è l'ereditarietà multipla, i mixin (ecc.) O come gestire la loro assenza. In qualsiasi lingua, anche nella lingua umana, una squadra non è un elenco di persone ma è composto da un elenco di persone. Questo è un concetto astratto. Se Bertrand Meyer o qualcuno gestisce una squadra in una sottoclasse, List sta sbagliando. Dovresti sottoclassare un Elenco se desideri un tipo più specifico di elenco. Spero che tu sia d'accordo
Mauro Sampietro,

1
Dipende da quale eredità è per te. Di per sé, è un'operazione astratta, non significa che due classi siano in una relazione "è un tipo speciale di", anche se è l'interpretazione principale. Tuttavia, l'ereditarietà può essere trattata come un meccanismo per il riutilizzo dell'implementazione se la progettazione del linguaggio lo supporta, anche se la composizione è l'alternativa perferita al giorno d'oggi.
Alexey,

2

Penso di non essere d'accordo con la tua generalizzazione. Una squadra non è solo una raccolta di giocatori. Una squadra ha molte più informazioni al riguardo: nome, emblema, raccolta del personale di gestione / amministrazione, raccolta della squadra di allenatori, quindi raccolta di giocatori. Quindi, correttamente, la tua classe FootballTeam dovrebbe avere 3 raccolte e non essere essa stessa una raccolta; se si tratta di modellare correttamente il mondo reale.

Puoi prendere in considerazione una classe PlayerCollection che, come Specialized StringCollection, offre alcune altre funzionalità, come la convalida e i controlli prima che gli oggetti vengano aggiunti o rimossi dall'archivio interno.

Forse, la nozione di scommettitori di PlayerCollection si adatta al tuo approccio preferito?

public class PlayerCollection : Collection<Player> 
{ 
}

E poi FootballTeam può apparire così:

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

2

Preferisci le interfacce rispetto alle classi

Le classi dovrebbero evitare di derivare dalle classi e implementare invece le interfacce minime necessarie.

L'ereditarietà rompe l'incapsulamento

Derivando dalle classi si rompe l'incapsulamento :

  • espone dettagli interni su come viene implementata la tua collezione
  • dichiara un'interfaccia (insieme di funzioni e proprietà pubbliche) che potrebbe non essere appropriata

Tra le altre cose, questo rende più difficile il refactoring del codice.

Le classi sono un dettaglio di implementazione

Le classi sono dettagli di implementazione che dovrebbero essere nascosti da altre parti del codice. In breve aSystem.List è un'implementazione specifica di un tipo di dati astratto, che può essere o non essere appropriato ora e in futuro.

Concettualmente il fatto che il System.Listtipo di dati sia chiamato "elenco" è un po 'un'aringa rossa. A System.List<T>è una raccolta ordinata mutabile che supporta le operazioni O (1) ammortizzate per l'aggiunta, l'inserimento e la rimozione di elementi e le operazioni O (1) per il recupero del numero di elementi o il recupero e l'impostazione dell'elemento per indice.

Più piccola è l'interfaccia, più flessibile è il codice

Quando si progetta una struttura di dati, più semplice è l'interfaccia, più flessibile è il codice. Guarda quanto è potente LINQ per una dimostrazione di questo.

Come scegliere le interfacce

Quando pensi "alla lista", dovresti iniziare dicendo a te stesso: "Devo rappresentare una raccolta di giocatori di baseball". Quindi diciamo che decidi di modellarlo con una classe. Quello che dovresti fare prima è decidere quale sarà la quantità minima di interfacce che questa classe dovrà esporre.

Alcune domande che possono aiutare a guidare questo processo:

  • Devo avere il conto? In caso contrario, considerare l'attuazioneIEnumerable<T>
  • Questa raccolta cambierà dopo essere stata inizializzata? Se non si consideraIReadonlyList<T> .
  • È importante poter accedere agli articoli per indice? Prendere in considerazioneICollection<T>
  • L'ordine in cui aggiungo elementi alla raccolta è importante? Forse è unISet<T> ?
  • Se vuoi davvero queste cose, vai avanti e attua IList<T>.

In questo modo non abbinerai altre parti del codice ai dettagli di implementazione della tua collezione di giocatori di baseball e sarai libero di cambiare il modo in cui viene implementato fintanto che rispetti l'interfaccia.

Adottando questo approccio, scoprirai che il codice diventa più facile da leggere, refactorizzare e riutilizzare.

Note su come evitare la caldaia

L'implementazione delle interfacce in un IDE moderno dovrebbe essere semplice. Fare clic con il tasto destro e selezionare "Implement Interface". Quindi, se necessario, inoltrare tutte le implementazioni a una classe membro.

Detto questo, se ti accorgi che stai scrivendo un sacco di boilerplate, è potenzialmente perché stai esponendo più funzioni di quelle che dovresti essere. È la stessa ragione per cui non dovresti ereditare da una classe.

Puoi anche progettare interfacce più piccole che abbiano un senso per la tua applicazione, e forse solo un paio di funzioni di estensione dell'helper per mappare quelle interfacce a tutte le altre di cui hai bisogno. Questo è l'approccio che ho adottato nella mia IArrayinterfaccia per la libreria LinqArray .


2

Problemi con la serializzazione

Manca un aspetto. Le classi che ereditano dall'elenco <> non possono essere serializzate correttamente usando XmlSerializer . In tal caso, è necessario utilizzare DataContractSerializer oppure è necessaria una propria implementazione di serializzazione.

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

Ulteriori letture: quando una classe viene ereditata da List <>, XmlSerializer non serializza altri attributi


0

Quando è accettabile?

Per citare Eric Lippert:

Quando stai costruendo un meccanismo che estende il List<T>meccanismo.

Ad esempio, sei stanco dell'assenza del AddRangemetodo in IList<T>:

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

public class MoreConvenientList<T> : List<T>, IMoreConvenientListInterface<T> { }
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.