c# - remove - htmlagilitypack




Perché non ereditare dalla lista<T>? (18)

Quando pianifico i miei programmi, spesso inizio con una catena di pensieri come questa:

Una squadra di calcio è solo una lista di giocatori di calcio. Pertanto, dovrei rappresentarlo con:

var football_team = new List<FootballPlayer>();

L'ordine di questa lista rappresenta l'ordine in cui i giocatori sono elencati nel roster.

Ma mi rendo conto più tardi che le squadre hanno anche altre proprietà, oltre alla semplice lista di giocatori, che devono essere registrate. Ad esempio, il totale parziale dei punteggi in questa stagione, il budget corrente, i colori uniformi, una string rappresenta il nome della squadra, ecc.

Allora penso:

Ok, una squadra di football è come una lista di giocatori, ma in aggiunta ha un nome (una string ) e un totale parziale di punteggi (un int ). .NET non fornisce una classe per la memorizzazione delle squadre di calcio, quindi farò la mia classe. La struttura esistente più simile e rilevante è 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 si dovrebbe ereditare dalla List<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 dovrei causare se estendo List ? Cosa si romperà esattamente?

Un'altra ragione che ho visto è che List è fornito da Microsoft e non ho alcun controllo su di esso, quindi non posso cambiarlo più tardi, 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 mai questa API pubblica, posso ignorare tranquillamente questa linea guida? Se eredito da List e risulta che ho bisogno di un'API pubblica, quali difficoltà dovrò avere?

Perché è ancora importante? Una lista è una lista. Cosa potrebbe cambiare? Cosa potrei voler cambiare?

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

Cos'altro dovrei usare?

Apparentemente, per le collezioni personalizzate, Microsoft ha fornito una classe Collection che dovrebbe essere estesa anziché List . Ma questa classe è molto semplice e non ha molte cose utili, come ad esempio AddRange . La risposta di jvitor83 fornisce un razionale di prestazioni per quel particolare metodo, ma come è un AddRange lento non migliore di nessun AddRange ?

Ereditare dalla Collection è molto più lavoro che ereditare dalla List , e non vedo alcun beneficio. Sicuramente Microsoft non mi direbbe di fare del lavoro extra senza motivo, quindi non posso fare a meno di sentirmi in qualche modo frainteso e l'ereditare Collection non è la soluzione giusta per il mio problema.

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

Infine, alcuni suggeriscono di avvolgere la List in qualcosa:

class FootballTeam 
{ 
    public List<FootballPlayer> Players; 
}

Ci sono due problemi con questo:

  1. Rende il mio codice inutilmente dettagliato. Ora devo chiamare my_team.Players.Count anziché solo my_team.Count . Fortunatamente, con C # posso definire gli indicizzatori per rendere l'indicizzazione trasparente e inoltrare tutti i metodi List interno ... Ma questo è un sacco di codice! Cosa ottengo per tutto ciò che funziona?

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

Mi rendo conto che ciò che accade "sotto il cofano" si può dire che è "aggiungere la lista interna di X a Y", ma questo sembra un modo molto controintuitivo di pensare al mondo.

La mia domanda (riassunta)

Qual è il modo corretto C # di rappresentare una struttura dati, che, "logicamente" (vale a dire, "per la mente umana") è solo una list di things con poche campane e fischietti?

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


Infine, alcuni suggeriscono di avvolgere la lista in qualcosa:

Questo è il modo corretto. "Stranamente verboso" è un brutto modo di guardare a questo. Ha un significato esplicito quando scrivi my_team.Players.Count . Vuoi contare i giocatori.

my_team.Count

..non significa niente. Conta cosa?

Una squadra non è una lista - il consistere di più di un semplice elenco di giocatori. Una squadra possiede giocatori, quindi i giocatori dovrebbero farne parte (un membro).

Se sei davvero preoccupato che sia eccessivamente prolisso, puoi sempre esporre le proprietà del team:

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

..che diventa:

my_team.PlayerCount

Questo ha un significato ora e aderisce a The Law Of Demeter .

Dovresti anche considerare di aderire al principio del riutilizzo composito . Tramite l'ereditarietà di List<T> , stai dicendo che una squadra è una lista di giocatori e che ne espone metodi inutili. Questo non è corretto - come hai affermato, una squadra è più di una lista di giocatori: ha un nome, manager, membri del consiglio di amministrazione, allenatori, personale medico, stipendi, ecc. Facendo in modo che la tua classe della squadra contenga un elenco di giocatori, Stai dicendo "Una squadra ha una lista di giocatori", ma può anche avere altre cose.


Qual è la corretta C # modo di rappresentare una struttura di dati ...

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

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

Scegli uno che è utile per te e / i tuoi utenti. Questo è tutto. Sviluppare economicamente, non sovra-ingegnerizzare. Meno codice scrivi, meno codice dovrai eseguire il debug. (leggi le seguenti edizioni).

-- Modificato

La mia migliore risposta sarebbe ... dipende. L'ereditarietà di un elenco esporrebbe i client di questa classe a metodi che potrebbero non essere esposti, principalmente perché FootballTeam si presenta come un'entità aziendale.

- Edizione 2

Sinceramente non ricordo a cosa mi riferivo nel commento "non sovra-ingegnere". 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 della perdita di astrazione .

D'altra parte, credo che ci sia un numero limitato di casi in cui è sufficiente ereditare dalla lista. Come ho scritto nella precedente edizione, 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.


Preferisci interfacce su classi

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

Interruzioni di eredità Incapsulamento

Derivando dall'incapsulamento delle pause delle lezioni :

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

Tra l'altro 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 a System.Listè un'implementazione specifica di un tipo di dati astratto, che può o non può essere appropriato ora e in futuro.

Concettualmente il fatto che il System.Listtipo di dati sia chiamato "elenco" è un po 'un falso rosso. A System.List<T>è una collezione ordinata mutevole che supporta operazioni O (1) ammortizzate per aggiungere, inserire e rimuovere elementi e operazioni O (1) per recuperare il numero di elementi o ottenere e impostare l'elemento per indice.

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

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

Come scegliere le interfacce

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

Alcune domande che possono aiutare a guidare questo processo:

  • Devo avere il conteggio? Se non considerare l'implementazioneIEnumerable<T>
  • Questa collezione cambierà dopo essere stata inizializzata? Se non lo consideri IReadonlyList<T>.
  • È importante poter accedere agli oggetti per indice? Tenere contoICollection<T>
  • L'ordine in cui aggiungo elementi alla collezione è importante? Forse è un ISet<T>?
  • Se davvero vuoi queste cose, vai avanti e attua IList<T>.

In questo modo non accoppierà altre parti del codice ai dettagli di implementazione della tua collezione di giocatori di baseball e sarai libero di cambiare il modo in cui è implementato, purché tu rispetti l'interfaccia.

Con questo approccio scoprirai che il codice diventa più facile da leggere, refactoring e riutilizzo.

Note sull'elusione di Boilerplate

Implementare interfacce in un moderno IDE dovrebbe essere facile. Fare clic con il tasto destro e selezionare "Implementa interfaccia". Quindi inoltra tutte le implementazioni a una classe membro, se necessario.

Detto questo, se trovi che stai scrivendo molte regole, è potenzialmente perché stai esponendo più funzioni di quanto dovresti essere. È la stessa ragione per cui non dovresti ereditare da una classe.

Puoi anche progettare interfacce più piccole che hanno senso per la tua applicazione, e forse solo un paio di funzioni di estensione dell'helper per mappare tali interfacce a qualsiasi altra di cui hai bisogno. Questo è l'approccio che ho preso nella mia IArrayinterfaccia per la libreria LinqArray .


Design> Implementazione

Quali metodi e proprietà esponi sono una decisione di progettazione. La classe base da cui si eredita è 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 mostra questo oggetto in quel modello?
  • Come potrebbe questo cambiamento in futuro?

Ricorda che l'ereditarietà implica una relazione "isa" (è a), mentre la composizione implica una relazione "ha a" (hasa). Scegli quello giusto per la tua situazione nella tua vista, tenendo presente dove le cose potrebbero andare 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 cervello in "modalità di progettazione" in questo modo.

Questo non è qualcosa che tutti fanno consapevolmente a questo livello nella codifica di tutti i giorni. Ma se stai rimuginando questo tipo di argomento, stai calpestando le acque del design. Essere consapevoli di ciò può essere liberatorio.

Prendi in considerazione le specifiche del design

Dai un'occhiata a List <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 un FootballTeam dal tuo punto di vista?

FootballTeam.Reverse () ha senso per te? FootballTeam.ConvertAll <TOutput> () assomiglia a qualcosa che vuoi?

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

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

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

Se vuoi che il tuo oggetto contenga una collezione / elenco di giocatori (dati), e quindi vuoi che la raccolta o l'elenco sia una proprietà o un membro, fallo assolutamente. fittiziamente:

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

Potresti sentire che vuoi che le persone siano in grado di enumerare solo il set 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 è fuorviante. Chiunque tenti di dirti che è categoricamente e definitivamente giusto in ogni caso è fuorviante.

Passare all'attuazione

Una volta che hai deciso i dati e il comportamento, puoi prendere una decisione sull'implementazione. Questo include le classi concrete da cui dipende l'ereditarietà o la composizione.

Questo potrebbe non essere un grande passo in avanti, e le persone spesso confondono design e implementazione, dal momento che è abbastanza possibile sfogarsi in testa in un secondo o due e iniziare a digitare.

Un esperimento di pensiero

Un esempio artificiale: come hanno già detto altri, una squadra non è sempre "solo" una collezione di giocatori. Mantenere una collezione 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 si finisce con:

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

Nonostante il design, a questo punto in C # non sarà possibile implementare tutto ciò ereditando comunque da List <T>, poiché C # "only" supporta l'ereditarietà singola. (Se hai provato questo malarky in C ++, si può considerare questa una buona cosa.) Implementazione di una raccolta tramite eredità e uno via la composizione rischia di sentirsi sporca. E proprietà come Count diventano confuse per gli utenti a meno che non implementi ILIst <Player> .Count e IList <StaffMember> .Count ecc. Esplicitamente, e quindi sono solo dolorosi piuttosto che confusi. Puoi vedere dove sta andando; sentirsi bene mentre si pensa questa via può ben dirvi che è sbagliato dirigersi in questa direzione (e a torto oa ragione, i vostri colleghi potrebbero anche averlo 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. È ricevuto saggezza non una legge. Una ragione è che nella pratica si considera che la composizione vinca spesso sull'eredità in termini di comprensibilità, implementazione e manutenibilità. È più comune con oggetti del mondo reale / dominio trovare relazioni "hasa" utili e consistenti rispetto alle relazioni "isa" utili e coerenti, a meno che non siate in profondità nell'astratto, soprattutto con il passare del tempo e i dati precisi e il comportamento degli oggetti nel codice i cambiamenti. Questo non dovrebbe farti escludere sempre l'ereditarietà dalle classi di raccolta; ma può essere suggestivo.


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

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

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

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

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

  • Una squadra di calcio è una lista 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 collezione di giocatori
    • L'ordine di inclusione dei giocatori è descrittivo dello stato della squadra e il team assicura che la sequenza venga conservata a meno che non venga esplicitamente modificato? 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 una lista è applicabile a una squadra. Quindi una squadra non è una lista. Un elenco sarebbe un dettaglio di implementazione di come gestisci il tuo team, quindi dovrebbe essere usato solo per memorizzare gli oggetti del giocatore ed essere manipolato con i metodi della classe Team.

A questo punto vorrei sottolineare che una classe di Team dovrebbe, a mio parere, non essere nemmeno implementata usando una Lista; dovrebbe essere implementato utilizzando una struttura di dati Set (ad esempio HashSet) nella maggior parte dei casi.


Cosa succede se FootballTeam ha una squadra riserve insieme alla squadra principale?

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

Come lo modellerai?

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

La relazione ha chiaramente a e non è a .

o RetiredPlayers ?

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

Come regola generale, se si desidera ereditare da una raccolta, denominare la classe SomethingCollection .

Semanticamente la tua SomethingCollection senso? Fallo solo se il tuo tipo è una raccolta di Something .

Nel caso di FootballTeam , non suona bene. Una Team è più di una Collection . Una Team può avere allenatori, allenatori, ecc. Come hanno sottolineato le altre risposte.

FootballCollection sembra una raccolta di palloni da calcio o forse una collezione di accessori per il calcio. TeamCollection , una raccolta di squadre.

FootballPlayerCollection sembra una raccolta di giocatori che sarebbe un nome valido per una classe che eredita da List<FootballPlayer> se davvero volevi farlo.

List<FootballPlayer> reale List<FootballPlayer> è un tipo perfettamente valido da affrontare. Forse IList<FootballPlayer> se lo stai restituendo da un metodo.

In sintesi

Chiedilo a te stesso

  1. X a Y ? o ha X a Y ?

  2. I miei nomi di classe significano cosa sono?


Questo è un classico esempio di composition vs inheritance .

In questo caso specifico:

La squadra è una lista di giocatori con un comportamento aggiunto

o

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

Estendendo List ti stai limitando in diversi modi:

  1. Non è possibile limitare l'accesso (ad esempio, impedire alle persone di cambiare il ruolo). Ottieni tutti i metodi di Lista se hai bisogno / li vuoi tutti o no.

  2. Cosa succede se vuoi avere anche elenchi di altre cose. Ad esempio, i team hanno allenatori, manager, tifosi, attrezzature, ecc. Alcuni di questi potrebbero essere elenchi a pieno titolo.

  3. Limiti le tue opzioni per l'ereditarietà. Ad esempio, potresti voler creare un oggetto Team generico e poi avere BaseballTeam, FootballTeam, ecc. Che erediti da quello. Per ereditare da List devi fare l'ereditarietà dal Team, ma questo significa che tutti i vari tipi di team sono costretti ad avere la stessa implementazione di quel ruolo.

Composizione: include un oggetto che fornisce il comportamento desiderato all'interno dell'oggetto.

Ereditarietà: il tuo oggetto diventa un'istanza dell'oggetto che ha il comportamento che desideri.

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


Wow, il tuo post ha un'intera serie di domande e punti. La maggior parte del ragionamento che ottieni da Microsoft è esattamente al punto. Iniziamo con tutto ciò che riguarda List<T>

  • List<T> è altamente ottimizzato. Il suo uso principale è quello di essere usato come membro privato di un oggetto.
  • Microsoft non lo ha sigillato perché a volte potresti voler creare una classe con un nome più amichevole: class MyList<T, TX> : List<CustomObject<T, Something<TX>> { ... } . Ora è facile come var list = new MyList<int, string>(); .
  • CA1002: Non esporre elenchi generici : in sostanza, anche se si prevede di utilizzare questa app come unico sviluppatore, vale la pena sviluppare con buone pratiche di codifica, in modo che si instillino in te e in una seconda natura. Sei ancora autorizzato a esporre l'elenco come un IList<T> se hai bisogno di un consumatore per avere un elenco indicizzato. Questo ti consente di modificare l'implementazione all'interno di una classe in seguito.
  • Microsoft ha reso Collection<T> molto generica perché è un concetto generico ... il nome dice tutto; è solo una collezione. Esistono versioni più precise come SortedCollection<T> , ObservableCollection<T> , ReadOnlyCollection<T> , ecc. Ognuna delle quali implementa IList<T> ma non List<T> .
  • Collection<T> consente ai membri (ad esempio Aggiungi, Rimuovi, ecc.) Di essere sovrascritti perché sono virtuali. List<T> no.
  • L'ultima parte della tua domanda è azzeccata. Una squadra di calcio è più di una semplice lista di giocatori, quindi dovrebbe essere una classe che contiene quella lista di giocatori. Pensa Composizione vs Eredità . Una squadra di calcio ha una lista di giocatori (un roster), non è un elenco di giocatori.

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

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

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

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 questo. In realtà, si vuole fare un ulteriore passo avanti e impedire attivamente ad altri oggetti di aggirare questi interni.

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

Supponiamo di voler differenziare quando un giocatore se ne va sapendo se è andato in pensione, ha rassegnato le dimissioni o è stato licenziato. È possibile implementare un RemovePlayermetodo che richiede un enum di input appropriato. Tuttavia, ereditando da List, si sarebbe in grado di impedire l'accesso diretto a Remove, RemoveAlle persino Clear. Di conseguenza, hai effettivamente disimpegnato la tua FootballTeamclasse.

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

Rende il mio codice inutilmente dettagliato. Ora devo chiamare my_team.Players.Count anziché solo my_team.Count.

Hai ragione, sarebbe inutilmente prolisso per tutti i clienti che usano la tua squadra. Tuttavia, questo problema è molto piccolo in confronto al fatto che hai esposto List Playersa tutti e diversi in modo da poter giocare con la tua squadra senza il tuo consenso.

Continui a dire:

Semplicemente non ha alcun senso. Una squadra di calcio non "ha" una lista di giocatori. È la lista dei giocatori. Non dici "John McFootballer è entrato nei giocatori di SomeTeam". Dici "John si è unito a SomeTeam".

Ti stai sbagliando per la prima parte: cancella la parola "elenco" ed è evidente che una squadra ha giocatori.
Tuttavia, hai colpito il chiodo sulla testa con il secondo. Non vuoi che i clienti chiamino ateam.Players.Add(...). Li vuoi chiamare ateam.AddPlayer(...). E la tua implementazione sarebbe (forse tra le altre cose) chiamata Players.Add(...)internamente.

Spero che tu possa vedere quanto sia importante l'incapsulamento per l'obiettivo di potenziare i tuoi oggetti. Vuoi consentire a ogni classe di svolgere bene il proprio lavoro senza temere interferenze da altri oggetti.


Fammi riscrivere la tua domanda. quindi potresti vedere il soggetto da una prospettiva diversa.

Quando ho bisogno di rappresentare una squadra di calcio, capisco che è fondamentalmente un nome. Come: "Le aquile"

string team = new string();

Poi più tardi ho realizzato che anche i team hanno giocatori.

Perché non posso semplicemente estendere il tipo di stringa in modo che tenga 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, puoi vedere se condivide le proprietà con altre classi. E pensa all'eredità.


Il mio sporco segreto: non mi interessa cosa dice la gente, e lo faccio. .NET Framework è distribuito con "XxxxCollection" (UIElementCollection per l'esempio migliore).

Quindi cosa mi impedisce di dire:

team.Players.ByName("Nicolas")

Quando lo trovo meglio di

team.ByName("Nicolas")

Inoltre, il mio PlayerCollection potrebbe essere utilizzato da un'altra classe, come "Club" senza alcuna duplicazione di codice.

club.Players.ByName("Nicolas")

Le migliori pratiche di ieri potrebbero non essere quelle di domani. Non c'è alcun motivo dietro la maggior parte delle migliori pratiche, la maggior parte sono solo un ampio consenso tra la comunità. Invece di chiedere alla comunità se ti darà la colpa quando lo chiedi a te stesso, 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 "pratica migliore" che arriva dal nulla.


Quando dicono che List<T>è "ottimizzato", penso che vogliano dire che non ha caratteristiche come i metodi virtuali che sono un po 'più costosi. Quindi il problema è che una volta che esponi la List<T>tua API pubblica , perdi la capacità di applicare le regole aziendali o di personalizzare la sua funzionalità in un secondo momento. Ma se stai utilizzando questa classe ereditata come interna all'interno del 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 dell'ereditarietà List<T>consiste nell'eliminare un sacco di codice wrapper stupido che non verrà mai personalizzato nel futuro prevedibile. Inoltre, se vuoi che la tua classe abbia esattamente la stessa semantica diList<T> per la vita delle tue API quindi potrebbe anche essere OK.

Spesso vedo molta gente fare un sacco di lavoro extra solo perché la regola FxCop lo dice o il blog di qualcuno dice che è una pratica "cattiva". Molte volte, questo si trasforma in codice per progettare la stranezza di palooza del modello. Come con molte linee guida, trattalo come linea guida che può avere eccezioni.


Quello che dicono le linee guida è che l'API pubblica non dovrebbe rivelare la decisione di progettazione interna se si sta utilizzando una lista, un set, un dizionario, un albero o altro. Una "squadra" non è necessariamente una lista. Puoi implementarlo come una lista ma gli utenti della tua API pubblica dovrebbero usare la tua classe sulla base della necessità di conoscere. Ciò ti consente di cambiare la tua decisione e utilizzare una diversa struttura dei dati senza influire sull'interfaccia pubblica.


Se gli utenti della classe hanno bisogno di tutti i metodi e le proprietà ** Elenco ha, si dovrebbe derivare la classe da esso. Se non ne hanno bisogno, racchiudi l'Elenco e crea wrapper per i metodi di cui i tuoi utenti di classe hanno realmente bisogno.

Questa è una regola rigorosa, se si scrive 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 piccole app, potresti anche considerare di scegliere un altro linguaggio meno rigoroso. Ruby, JavaScript - tutto ciò che ti permette di scrivere meno codice.


Solo perché penso che l'altro risponda praticamente alla tangente di una squadra di calcio "è-un" List<FootballPlayer>o "ha-un" List<FootballPlayer>, che in realtà non risponde a questa domanda come scritto.

Il PO chiede principalmente chiarimenti sulle linee guida per ereditare da List<T>:

Una linea guida dice che non si dovrebbe ereditare da 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 poco dolore, 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 ai programmatori di terze parti. Pensa al codice quadro. E ricorda che le linee guida a cui si fa riferimento sono " Linee guida per la progettazione di .NET Framework " e non " Linee guida per la progettazione di applicazioni .NET ". C'è una differenza e, in generale, la progettazione dell'API pubblica è molto più rigida.

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

Praticamente, sì. Puoi prendere in considerazione la logica alla base per vedere se si applica comunque alla tua situazione, ma se non stai creando un'API pubblica allora non devi preoccuparti in particolare delle problematiche relative alle API come il controllo delle versioni (di cui, questo è un sottoinsieme).

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

Perché è ancora importante? Una lista è una lista. Cosa potrebbe cambiare? Cosa potrei voler cambiare?

Dipende dal contesto, ma dal momento che stiamo usando FootballTeamcome esempio - immagina che non puoi aggiungere un FootballPlayerse questo causerebbe il superamento del tetto salariale della squadra. Un possibile modo di aggiungere sarebbe qualcosa di simile:

 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 ragioni di prestazioni).

Se sei in un'applicazione (che, in pratica, significa che tu e tutti i chiamanti sono stati compilati insieme), ora puoi passare all'utilizzo 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 hai esposto pubblicamente a una terza parte, hai appena apportato una brusca modifica che causerà errori di compilazione e / o di esecuzione.

TL; DR - le linee guida sono per le API pubbliche. Per le API private, fai ciò che vuoi.


Volevo solo aggiungere che Bertrand Meyer, l'inventore di Eiffel e design per contratto, avrebbe Teamereditato dall'esterno List<Player>senza nemmeno 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 rinomina delle funzionalità . In C #, quando si sottoclasse, si eredita sia l'interfaccia che l'implementazione. È possibile sovrascrivere 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 Removeda Hiree Firenel proprio Team. Se un'istanza di Teamè upcast torna a List<Player>, il chiamante utilizza Adde Removemodificarlo, ma i vostri metodi virtuali Hiree Firesarà chiamato.


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

Codice precedente significa: un gruppo di ragazzi della strada che giocano a calcio, e capita di avere un nome. Qualcosa di simile a:

Ad ogni modo, questo codice (dalla mia risposta)

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

Mezzi: questa è una squadra di calcio che ha dirigenti, giocatori, amministratori, ecc. Qualcosa del tipo:

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







inheritance