Dlaczego nie odziedziczyć po liście <T>?


1398

Planując swoje programy, często zaczynam od takiego łańcucha myśli:

Drużyna piłkarska to tylko lista piłkarzy. Dlatego powinienem to przedstawić za pomocą:

var football_team = new List<FootballPlayer>();

Kolejność na tej liście odpowiada kolejności, w jakiej gracze są umieszczeni na liście.

Ale później zdaję sobie sprawę, że drużyny mają także inne właściwości, poza samą listą graczy, które muszą zostać zarejestrowane. Na przykład bieżąca suma wyników w tym sezonie, aktualny budżet, jednolite kolory, stringnazwa drużyny itp.

Więc myślę:

W porządku, drużyna piłkarska jest jak lista zawodników, ale dodatkowo ma nazwę (a string) i sumę wyników (an int). .NET nie zapewnia klasy do przechowywania drużyn piłkarskich, więc stworzę własną klasę. Najbardziej podobna i odpowiednia istniejąca struktura jest List<FootballPlayer>, więc odziedziczę po niej:

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

Ale okazuje się, że wytyczna mówi, że nie powinieneś dziedziczyćList<T> . Jestem całkowicie zdezorientowany tymi wytycznymi pod dwoma względami.

Dlaczego nie?

Najwyraźniej Listjest jakoś zoptymalizowany pod kątem wydajności . Jak to? Jakie problemy z wydajnością spowoduję, jeśli przedłużę List? Co dokładnie się złamie?

Innym powodem, który widziałem, jest ten, który Listzapewnia Microsoft i nie mam nad nim kontroli, więc nie mogę go później zmienić po ujawnieniu „publicznego interfejsu API” . Ale staram się to zrozumieć. Co to jest publiczny interfejs API i dlaczego powinienem się tym przejmować? Jeśli mój obecny projekt nie ma i prawdopodobnie nie będzie miał tego publicznego interfejsu API, czy mogę bezpiecznie zignorować tę wytyczną? Jeśli odziedziczę List i okaże się, że potrzebuję publicznego interfejsu API, jakie będę miał trudności?

Dlaczego to w ogóle ma znaczenie? Lista jest listą. Co może się zmienić? Co mógłbym chcieć zmienić?

I na koniec, jeśli Microsoft nie chciał, żebym odziedziczył po nim List, dlaczego nie stworzyli klasy sealed?

Czego jeszcze mam użyć?

Najwyraźniej dla niestandardowych kolekcji Microsoft zapewnił Collectionklasę, którą należy rozszerzyć zamiast List. Ale ta klasa jest bardzo naga i nie ma wielu przydatnych rzeczy, takich jakAddRange na przykład. Odpowiedź jvitor83 zapewnia uzasadnienie wydajności dla tej konkretnej metody, ale w jaki sposób spowolnienie AddRangenie jest lepsze niż brak AddRange?

Dziedziczenie po Collectionto dużo więcej pracy niż dziedziczenie Listi nie widzę żadnej korzyści. Z pewnością Microsoft nie kazałby mi wykonywać dodatkowej pracy bez powodu, więc nie mogę się oprzeć wrażeniu, że coś w jakiś sposób nie rozumiem, a dziedziczenie Collectionnie jest właściwym rozwiązaniem dla mojego problemu.

Widziałem takie sugestie jak wdrożenie IList. Po prostu nie. To jest dziesiątki wierszy kodu bojlera, który nic mi nie daje.

Na koniec niektórzy sugerują zawinięcie Listczegoś w:

class FootballTeam 
{ 
    public List<FootballPlayer> Players; 
}

Są z tym dwa problemy:

  1. To sprawia, że ​​mój kod jest niepotrzebnie pełny. Muszę teraz zadzwonić my_team.Players.Countzamiast po prostu my_team.Count. Na szczęście za pomocą C # mogę zdefiniować indeksatory, aby indeksowanie było przejrzyste i przekazywać wszystkie metody wewnętrzne List... Ale to dużo kodu! Co dostanę za całą tę pracę?

  2. To po prostu nie ma sensu. Drużyna futbolowa nie ma „listy graczy”. To jest lista graczy. Nie mówisz: „John McFootballer dołączył do graczy SomeTeam”. Mówisz „John dołączył do SomeTeam”. Nie dodajesz litery do „znaków łańcucha”, dodajesz literę do łańcucha. Nie dodajesz książki do książek w bibliotece, dodajesz książkę do biblioteki.

Zdaję sobie sprawę, że to, co dzieje się „pod maską”, można powiedzieć, że „dodaje X do wewnętrznej listy Y”, ale wydaje się to bardzo sprzecznym z intuicją sposobem myślenia o świecie.

Moje pytanie (podsumowane)

Jaka jest prawidłowa C # sposób przedstawiający strukturę danych, która „logicznie” (to znaczy „dla ludzkiego umysłu”) jest tylko listz thingskilkoma wodotryski?

Czy dziedziczenie po List<T>zawsze jest nie do przyjęcia? Kiedy to jest dopuszczalne? Dlaczego? Dlaczego nie? Co musi wziąć pod uwagę programista, podejmując decyzję o dziedziczeniu List<T>czy nie?


90
Czy twoje pytanie naprawdę dotyczy tego, kiedy użyć dziedziczenia (Kwadrat jest prostokątem, ponieważ zawsze uzasadnione jest podanie kwadratu, gdy zażądano ogólnego prostokąta) i kiedy użyć kompozycji (dodatkowo FootballTeam ma Listę graczy w piłkę nożną do innych właściwości, które są dla niego tak „fundamentalne”, jak nazwa)? Czy inni programiści byliby zdezorientowani, gdyby gdzie indziej w kodzie przekazaliście im FootballTeam, gdy oczekiwali prostej listy?
Flight Odyssey,

77
co jeśli powiem ci, że drużyna piłkarska jest firmą biznesową, która WŁASNA jest lista umów z graczami zamiast goła lista graczy z dodatkowymi właściwościami?
STT LCU

75
To naprawdę bardzo proste. Czy drużyna piłkarska to skład zawodników? Oczywiście, że nie, ponieważ, jak mówisz, istnieją inne istotne właściwości. Czy drużyna piłkarska ma listę zawodników? Tak, więc powinna zawierać listę. Poza tym kompozycja jest po prostu lepsza niż dziedziczenie, ponieważ łatwiej ją później zmodyfikować. Jeśli uznasz, że dziedziczenie i kompozycja są logiczne jednocześnie, przejdź do kompozycji.
Tobberoth

45
@FlightOdyssey: Załóżmy, że masz metodę SquashRectangle, która przyjmuje prostokąt, zmniejsza jego połowę i podwaja szerokość. Teraz przechodzą w prostokącie, że dzieje się 4x4 ale jest typu prostokąta i kwadratu czyli 4x4. Jakie są wymiary obiektu po wywołaniu SquashRectangle? Dla prostokąta wyraźnie jest to 2x8. Jeśli kwadrat ma 2x8, to nie jest już kwadratem; jeśli nie jest to 2x8, to ta sama operacja na tym samym kształcie daje inny wynik. Wniosek jest taki, że zmienny kwadrat nie jest rodzajem prostokąta.
Eric Lippert,

29
@Gangnus: Twoje oświadczenie jest po prostu fałszywe; prawdziwa podklasa musi mieć funkcjonalność, która jest nadzbiorem nadklasy. A stringjest wymagane, aby zrobić wszystko, co objectmoże zrobić i więcej .
Eric Lippert,

Odpowiedzi:


1564

Tutaj jest kilka dobrych odpowiedzi. Dodałbym do nich następujące punkty.

Jaki jest prawidłowy sposób C # reprezentowania struktury danych, która „logicznie” (to znaczy „dla ludzkiego umysłu”) jest tylko listą rzeczy z kilkoma dzwonkami i gwizdkami?

Poproś dowolne dziesięć osób niebędących programistami komputerowymi, które są obeznane z istnieniem futbolu, o wypełnienie pustego pola:

Drużyna piłkarska jest szczególnym rodzajem _____

Czy ktoś powiedział „listę piłkarzy z kilkoma dzwonkami i gwizdkami”, czy też wszyscy powiedzieli „drużyna sportowa”, „klub” lub „organizacja”? Twoje wyobrażenie, że drużyna piłkarska jest szczególnym rodzajem listy graczy, leży w twoim ludzkim umyśle i tylko w twoim ludzkim umyśle.

List<T>jest mechanizmem . Drużyna piłkarska to obiekt biznesowy - to znaczy obiekt reprezentujący pewną koncepcję, która znajduje się w domenie biznesowej programu. Nie mieszaj ich! Drużyna piłkarska jest rodzajem drużyny; to ma dyżurów, A lista jest listą graczy . Lista nie jest szczególnym rodzajem listy graczy . Lista to lista graczy. Stwórz więc właściwość o nazwie Rosterto List<Player>. I zrób to, ReadOnlyList<Player>gdy jesteś przy nim, chyba że uważasz, że każdy, kto wie o drużynie piłkarskiej, może usunąć graczy z listy.

Czy dziedziczenie po List<T>zawsze jest nie do przyjęcia?

Nie do przyjęcia dla kogo? Mnie? Nie.

Kiedy to jest dopuszczalne?

Kiedy budujesz mechanizm, który rozszerza ten List<T>mechanizm .

Co musi wziąć pod uwagę programista, podejmując decyzję o dziedziczeniu List<T>czy nie?

Czy buduję mechanizm lub obiekt biznesowy ?

Ale to dużo kodu! Co dostanę za całą tę pracę?

Spędziłeś więcej czasu na wpisywaniu pytania, które zajęłoby Ci napisanie metod przekazywania odpowiednich członków List<T>pięćdziesiąt razy. Najwyraźniej nie boisz się gadatliwości, a tutaj mówimy o bardzo małej ilości kodu; to kilka minut pracy.

AKTUALIZACJA

Zastanowiłem się nad tym i jest jeszcze jeden powód, aby nie modelować drużyny futbolowej jako listy zawodników. W rzeczywistości może to być zły pomysł, aby modelować drużynę piłkarską jako posiadające listę graczy też. Problem z drużyną jako / posiadającą listę graczy polega na tym, że masz tylko migawkę drużyny w danym momencie . Nie wiem, jakie jest twoje uzasadnienie biznesowe dla tej klasy, ale gdybym miał klasę reprezentującą drużynę piłkarską, chciałbym zadać jej pytania takie jak: „ilu graczy Seahawks opuściło mecze z powodu kontuzji między 2003 a 2013 r.?” lub „Który zawodnik Denver, który wcześniej grał w innej drużynie, miał największy wzrost stoczni z roku na rok?” lub „ Czy Piggers przeszli całą drogę w tym roku?

Oznacza to, że drużyna piłkarska wydaje się być dobrze modelowana jako zbiór faktów historycznych, takich jak rekrutacja zawodnika, kontuzja, emerytura itp. Oczywiście obecny skład zawodników jest ważnym faktem, który prawdopodobnie powinien być na pierwszej linii centrum, ale mogą być inne ciekawe rzeczy, które chcesz zrobić z tym obiektem, które wymagają bardziej historycznej perspektywy.


84
Chociaż pozostałe odpowiedzi były bardzo pomocne, myślę, że ta odpowiedź dotyczy moich obaw najbardziej bezpośrednio. Jeśli chodzi o przesadzanie ilości kodu, masz rację, że ostatecznie nie jest to tak dużo pracy, ale bardzo łatwo się mylę, gdy dołączam jakiś kod do mojego programu, nie rozumiejąc, dlaczego go uwzględniam.
Superbest

127
@Superbest: Cieszę się, że mogłem pomóc! Masz rację, słuchając tych wątpliwości; jeśli nie rozumiesz, dlaczego piszesz jakiś kod, wymyśl go lub napisz inny kod, który rozumiesz.
Eric Lippert,

48
@ Mehrdad Ale wydaje się, że zapominasz o złotych zasadach optymalizacji: 1) nie rób tego, 2) nie rób tego jeszcze, i 3) nie rób tego bez uprzedniego wykonania profili wydajności, aby pokazać, co wymaga optymalizacji.
AJMansfield

107
@Mehrdad: Szczerze mówiąc, jeśli twoja aplikacja wymaga dbania o wydajność, powiedzmy, metod wirtualnych, to żaden współczesny język (C #, Java itp.) Nie jest dla ciebie językiem. Mam tutaj laptopa, który mógłby uruchomić symulację każdej gry zręcznościowej, w którą grałem jako dziecko jednocześnie . To pozwala mi nie martwić się o poziom pośredni tu i tam.
Eric Lippert,

44
@ Superbest: Teraz zdecydowanie jesteśmy na końcu spektrum „kompozycja nie dziedziczenie”. Rakieta składa się z części rakiety . Rakieta nie jest „specjalną listą części”! Sugerowałbym wam, abyście to jeszcze przycięli; Dlatego lista, w przeciwieństwie do IEnumerable<T>- sekwencja ?
Eric Lippert

161

Wow, twój post ma całą masę pytań i punktów. Większość argumentów uzyskanych od Microsoft jest dokładnie na miejscu. Zacznijmy od wszystkiegoList<T>

  • List<T> jest wysoce zoptymalizowany. Jego główne zastosowanie ma być używane jako prywatny członek obiektu.
  • Microsoft nie uszczelnić, bo czasami może chcesz utworzyć klasę, która ma nazwę friendlier: class MyList<T, TX> : List<CustomObject<T, Something<TX>> { ... }. Teraz jest to tak proste jak robienie var list = new MyList<int, string>();.
  • CA1002: Nie ujawniaj ogólnych list : Zasadniczo, nawet jeśli planujesz używać tej aplikacji jako jedynego programisty, warto rozwijać się z dobrymi praktykami kodowania, aby stały się zaszczepione w tobie i drugiej naturze. Nadal możesz ujawniać listę tak IList<T>, jakbyś potrzebował dowolnego konsumenta, aby mieć listę indeksowaną. To pozwoli ci później zmienić implementację w klasie.
  • Microsoft stał się Collection<T>bardzo ogólny, ponieważ jest to ogólna koncepcja ... nazwa mówi wszystko; to tylko kolekcja. Są bardziej precyzyjne wersje takie jak SortedCollection<T>, ObservableCollection<T>,ReadOnlyCollection<T> , itd., Z których każdy wdrażają IList<T>ale nie List<T> .
  • Collection<T>pozwala na zastąpienie członków (tj. Dodaj, Usuń itp.), ponieważ są wirtualni. List<T>nie.
  • Ostatnia część twojego pytania jest na miejscu. Drużyna piłkarska to coś więcej niż lista graczy, więc powinna to być klasa zawierająca tę listę graczy. Pomyśl Kompozycja a Dziedziczenie . Drużyna piłkarska ma listę graczy (grafik), nie jest to lista graczy.

Gdybym pisał ten kod, klasa prawdopodobnie wyglądałaby mniej więcej tak:

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
Twój post zawiera całą masę pytań ” - Powiedziałem, że byłem całkowicie zdezorientowany. Dzięki za link do pytania @ Readonly, teraz widzę, że duża część mojego pytania jest szczególnym przypadkiem bardziej ogólnego problemu Kompozycja vs. Dziedziczenie.
Superbest

3
@Brian: Poza tym, że nie można utworzyć ogólnego przy użyciu aliasu, wszystkie typy muszą być konkretne. W moim przykładzie List<T>jest on rozszerzony jako prywatna klasa wewnętrzna, która używa ogólnych, aby uprościć użycie i deklarację List<T>. Gdyby rozszerzenie było jedynie class FootballRoster : List<FootballPlayer> { }, to tak, mógłbym zamiast tego użyć aliasu. Myślę, że warto zauważyć, że można dodać dodatkowych członków / funkcje, ale jest to ograniczone, ponieważ nie można zastąpić ważnych członków List<T>.
mój

6
Gdyby to był Programmers.SE, zgodziłbym się z obecnym zakresem głosów odpowiedzi, ale ponieważ jest to wiadomość typu SO, ta odpowiedź przynajmniej próbuje odpowiedzieć na konkretne problemy w języku C # (.NET), mimo że istnieją określone ogólne problemy projektowe.
Mark Hurd

7
Collection<T> allows for members (i.e. Add, Remove, etc.) to be overriddenTeraz , że jest to dobry powód, aby nie dziedziczyć z listy. Pozostałe odpowiedzi mają zbyt wiele filozofii.
Navin

10
@my: Podejrzewam, że masz na myśli „mnóstwo” pytań, ponieważ sługa to detektyw.
batty

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

Poprzedni kod oznacza: grupa facetów z ulicy grających w piłkę nożną, a oni mają imię. Coś jak:

Ludzie grający w piłkę nożną

W każdym razie ten kod (z mojej odpowiedzi)

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

Oznacza: to drużyna piłkarska, która ma kierownictwo, zawodników, administratorów itp. Coś w stylu:

Obraz przedstawiający członków (i inne atrybuty) zespołu Manchester United

Oto jak twoja logika jest przedstawiona na zdjęciach…


9
Spodziewałbym się, że twoim drugim przykładem jest klub piłkarski, a nie drużyna piłkarska. Klub piłkarski ma menedżerów i administratora itp. Z tego źródła : „Drużyna piłkarska to zbiorowa nazwa nadana grupie zawodników ... Takie drużyny można wybrać do gry w meczu z drużyną przeciwną, aby reprezentować klub piłkarski ... ”. Moim zdaniem drużyna to lista graczy (a dokładniej zbiór graczy).
Ben

3
Bardziej zoptymalizowanyprivate readonly List<T> _roster = new List<T>(44);
SiD,

2
Konieczne jest zaimportowanie System.Linqprzestrzeni nazw.
Davide Cannizzo,

1
Wizualne: +1
V.7

115

Jest to klasyczny przykład kompozycji vs dziedziczenia .

W tym konkretnym przypadku:

Czy zespół to lista graczy z dodatkowym zachowaniem

lub

Czy zespół jest własnym obiektem, który zawiera listę graczy.

Rozszerzając listę, ograniczasz się na kilka sposobów:

  1. Nie możesz ograniczyć dostępu (na przykład powstrzymując ludzi przed zmianą listy). Otrzymujesz wszystkie metody List, niezależnie od tego, czy potrzebujesz ich wszystkich, czy nie.

  2. Co się stanie, jeśli chcesz mieć również listy innych rzeczy. Na przykład zespoły mają trenerów, menedżerów, fanów, sprzęt itp. Niektóre z nich mogą być osobnymi listami.

  3. Ograniczasz opcje dziedziczenia. Na przykład możesz chcieć utworzyć ogólny obiekt Team, a następnie odziedziczyć po nim BaseballTeam, FootballTeam itp. Aby dziedziczyć z Listy, musisz wykonać dziedziczenie od Zespołu, ale to oznacza, że ​​wszystkie różne typy zespołów są zmuszone do tej samej implementacji tego harmonogramu.

Kompozycja - w tym obiekt dający pożądane zachowanie wewnątrz obiektu.

Dziedziczenie - Twój obiekt staje się instancją obiektu o pożądanym zachowaniu.

Oba mają swoje zastosowania, ale jest to wyraźny przypadek, w którym kompozycja jest lepsza.


1
Rozwijając numer 2, nie ma sensu, aby Lista miała listę.
Cruncher

Po pierwsze, pytanie nie ma nic wspólnego z kompozycją a dziedziczeniem. Odpowiedź brzmi: OP nie chce zaimplementować bardziej szczegółowego rodzaju listy, więc nie powinien rozszerzać Listy <>. Jestem zakręcony, ponieważ masz bardzo wysokie wyniki w przepełnieniu stosu i powinieneś to jasno wiedzieć, a ludzie ufają temu, co mówisz, więc do tej pory min. 55 osób, które głosowały i których pomysł jest zdezorientowany, wierzą, że sposób jest inny system, ale oczywiście nie jest! # no-rage
Mauro Sampietro,

6
@sam On chce zachowania listy. Ma dwie możliwości, może przedłużyć listę (dziedziczenie) lub może dołączyć listę do swojego obiektu (kompozycja). Być może źle zrozumiałeś część pytania lub odpowiedzi, a nie 55 osób myli się i masz rację? :)
Tim B

4
Tak. Jest to zdegenerowana sprawa z tylko jednym super i sub, ale podam bardziej ogólne wyjaśnienie jego konkretnego pytania. Może mieć tylko jedną drużynę, która zawsze może mieć jedną listę, ale nadal pozostaje dziedziczenie listy lub włączenie jej jako obiektu wewnętrznego. Gdy zaczniesz uwzględniać wiele typów drużyn (piłka nożna. Krykiet. Etc) i będą one czymś więcej niż tylko listą graczy, zobaczysz, jak masz pełny skład w porównaniu do pytania o dziedziczenie. Spojrzenie na duży obraz jest ważne w takich przypadkach, aby uniknąć wcześniejszych złych wyborów, co później oznacza dużo refaktoryzacji.
Tim B,

3
OP nie poprosił o Kompozycję, ale przypadek użycia jest wyraźnym przykładem problemu X: Y, w którym jedna z 4 obiektowych zasad programowania jest zepsuta, niewłaściwie użyta lub nie w pełni zrozumiana. Bardziej wyraźną odpowiedzią jest albo napisanie specjalnej kolekcji, takiej jak stos lub kolejka (która nie wydaje się pasować do przypadku użycia), lub zrozumienie składu. Drużyna futbolowa NIE jest listą piłkarzy. To, czy OP poprosił o to wprost, czy nie, nie ma znaczenia, bez zrozumienia Abstrakcji, Enkapsulacji, Dziedziczenia i Polimorfizmu nie zrozumieją odpowiedzi, ergo, X: Y amok
Julia McGuigan

67

Jak wszyscy zauważyli, zespół graczy nie jest listą graczy. Ten błąd popełnia wiele osób na całym świecie, być może na różnych poziomach wiedzy. Często problem jest subtelny, a czasami bardzo poważny, jak w tym przypadku. Takie projekty są złe, ponieważ naruszają zasadę substytucji Liskowa . W Internecie znajduje się wiele dobrych artykułów wyjaśniających tę koncepcję, np. Http://en.wikipedia.org/wiki/Liskov_substitution_principle

Podsumowując, istnieją dwie reguły, które należy zachować w relacji rodzic / dziecko między klasami:

  • Dziecko nie powinno wymagać żadnych cech mniejszych niż to, co całkowicie definiuje Rodzica.
  • Rodzic nie powinien wymagać żadnych cech oprócz tego, co całkowicie definiuje Dziecko.

Innymi słowy, rodzic jest niezbędną definicją dziecka, a dziecko jest wystarczającą definicją rodzica.

Oto sposób na przemyślenie jednego z rozwiązań i zastosowanie powyższej zasady, która powinna pomóc uniknąć takiego błędu. Tę hipotezę należy przetestować, sprawdzając, czy wszystkie operacje klasy nadrzędnej są poprawne dla klasy pochodnej zarówno strukturalnie, jak i semantycznie.

  • Czy drużyna piłkarska to lista piłkarzy? (Czy wszystkie właściwości listy dotyczą zespołu w tym samym znaczeniu)
    • Czy zespół to zbiór jednorodnych bytów? Tak, zespół to kolekcja Graczy
    • Czy kolejność włączania graczy opisuje stan drużyny i czy zespół zapewnia zachowanie sekwencji, chyba że zostanie wyraźnie zmieniona? Nie i nie
    • Czy oczekuje się, że gracze zostaną uwzględnieni / upuszczeni na podstawie swojej kolejnej pozycji w zespole? Nie

Jak widać, tylko pierwsza cecha listy dotyczy zespołu. Dlatego zespół nie jest listą. Lista byłaby szczegółem implementacyjnym sposobu zarządzania zespołem, więc powinna być używana tylko do przechowywania obiektów gracza i manipulowania metodami klasy Team.

W tym miejscu chciałbym zauważyć, że klasa Drużyny nie powinna moim zdaniem nawet zostać zaimplementowana przy użyciu Listy; w większości przypadków powinien zostać zaimplementowany przy użyciu struktury danych Set (na przykład HashSet).


8
Niezły chwyt na liście kontra zestaw. Wydaje się, że jest to błąd popełniony zbyt często. Gdy w kolekcji może znajdować się tylko jedna instancja elementu, jakiś zestaw lub słownik powinien być preferowanym kandydatem do implementacji. Można mieć drużynę złożoną z dwóch graczy o tej samej nazwie. Nie jest w porządku, aby jeden gracz był włączony dwa razy w tym samym czasie.
Fred Mitchell,

1
+1 za niektóre dobre punkty, choć ważniejsze jest pytanie „czy struktura danych może w uzasadniony sposób obsługiwać wszystko, co przydatne (najlepiej krótko- i długoterminowe)”, zamiast „czy struktura danych robi więcej niż jest przydatna”.
Tony Delroy

1
@TonyD Cóż, podnoszę te kwestie, że nie należy sprawdzać, czy „struktura danych robi więcej niż to, co jest przydatne”. Ma to na celu sprawdzenie „Jeśli struktura danych rodzica robi coś, co jest nieistotne, pozbawione znaczenia lub sprzeczne z intuicją w stosunku do zachowania, które może sugerować klasa Child”.
Satyan Raina

1
@TonyD W rzeczywistości istnieje problem z nieistotnymi cechami pochodzącymi od rodzica, ponieważ w wielu przypadkach nie przejdzie negatywnych testów. Programista może rozszerzyć Human {eat (); biegać(); write ();} z goryla {eat (); biegać(); swing ();} myśląc, że nie ma nic złego w człowieku z dodatkową cechą zdolności bicia. A potem w świecie gry twój człowiek nagle omija wszystkie rzekome przeszkody lądowe, po prostu kołysząc się nad drzewami. O ile nie zostało to wyraźnie określone, praktyczny człowiek nie powinien być w stanie się kołysać. Taki projekt sprawia, że ​​API jest bardzo
podatne

1
@TonyD Nie sugeruję też, że klasa Player powinna pochodzić z HashSet. Sugeruję, że klasa Player powinna być „w większości przypadków” zaimplementowana przy użyciu HashSet za pośrednictwem Composition. Jest to całkowicie szczegół na poziomie implementacji, a nie projekt na poziomie projektowym (dlatego wspomniałem o tym jako sidenote dla mojej odpowiedzi). Można to bardzo dobrze zaimplementować za pomocą listy, jeżeli istnieje uzasadnione uzasadnienie takiej implementacji. Aby odpowiedzieć na pytanie, czy konieczne jest wyszukiwanie według O (1) według klucza? Nie. Dlatego NIE należy również rozszerzać odtwarzacza z HashSet.
Satyan Raina

50

Co jeśli FootballTeamzespół rezerwowy wraz z zespołem głównym?

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

Jak byś to modelował?

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

Związek jest wyraźnie , a nie jest .

czy RetiredPlayers?

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

Zasadniczo, jeśli chcesz dziedziczyć po kolekcji, nazwij klasę SomethingCollection .

Czy twój SomethingCollectionsemantycznie ma sens? Zrób to tylko, jeśli Twój typ to zbiórSomething .

W przypadku FootballTeamtego nie brzmi dobrze. TeamJest ponad Collection. ZATeamMoże mieć trenerów, trenerów, itp, jak inne odpowiedzi nie zauważył.

FootballCollection brzmi jak kolekcja piłek nożnych, a może kolekcja akcesoriów piłkarskich. TeamCollection, zbiór zespołów.

FootballPlayerCollection brzmi jak zbiór graczy, który byłby prawidłową nazwą dla klasy, która dziedziczy List<FootballPlayer> jeśli naprawdę chcesz to zrobić.

Naprawdę List<FootballPlayer>jest to bardzo dobry typ do radzenia sobie. MożeIList<FootballPlayer> jeśli zwracasz go z metody.

W podsumowaniu

Zapytaj siebie

  1. Czy ? lub Has ?XY XY

  2. Czy moje nazwy klas oznaczają, jakie są?


Jeśli typ każdego gracza będzie sklasyfikować je jako należące do jednej DefensivePlayers, OffensivePlayersalbo OtherPlayers, że może być legalnie przydatne typ, który mógłby być wykorzystany przez kod, który spodziewa się List<Player>, ale również ujęte członków DefensivePlayers, OffsensivePlayerslub SpecialPlayerstypu IList<DefensivePlayer>, IList<OffensivePlayer>oraz IList<Player>. Można użyć osobnego obiektu do buforowania oddzielnych list, ale umieszczenie ich w tym samym obiekcie, co główna lista, wydaje się być czystsze [użyj unieważnienia modułu wyliczającego listy ...
supercat

... jako wskazówka na fakt, że główna lista uległa zmianie, a listy podrzędne będą musiały zostać zregenerowane przy następnym dostępie].
supercat

2
Chociaż zgadzam się z tym stwierdzeniem, naprawdę rozdziera moją duszę, widząc, jak ktoś udziela porad projektowych i sugeruje odsłonięcie konkretnej Listy <T> z publicznym narzędziem pobierającym i ustawiającym w obiekcie biznesowym :(
sara

38

Projekt> Implementacja

Jakie metody i właściwości ujawniasz, to decyzja projektowa. Dziedziczoną przez ciebie klasą podstawową jest szczegół implementacji. Uważam, że warto cofnąć się do poprzedniego.

Obiekt to zbiór danych i zachowania.

Więc twoje pierwsze pytania powinny być:

  • Jakie dane zawiera ten obiekt w modelu, który tworzę?
  • Jakie zachowanie wykazuje ten obiekt w tym modelu?
  • Jak może się to zmienić w przyszłości?

Należy pamiętać, że dziedziczenie oznacza relację „isa” (jest a), podczas gdy kompozycja oznacza relację „ma” (hasa). Wybierz odpowiedni dla swojej sytuacji, biorąc pod uwagę, gdzie może się potoczyć rozwój aplikacji.

Zastanów się nad myśleniem w interfejsach, zanim zaczniesz myśleć w konkretnych typach, ponieważ niektórym ludziom łatwiej jest w ten sposób przełączyć mózg w „tryb projektowania”.

To nie jest coś, co każdy robi świadomie na tym poziomie w codziennym kodowaniu. Ale jeśli zastanawiasz się nad tym tematem, stąpasz po wodach projektowych. Świadomość tego może być wyzwalająca.

Rozważ specyfikę projektu

Spójrz na List <T> i IList <T> w MSDN lub Visual Studio. Zobacz, jakie metody i właściwości ujawniają. Czy według ciebie wszystkie te metody wyglądają na coś, co ktoś chciałby zrobić z drużyną piłkarską?

Czy footballTeam.Reverse () ma dla Ciebie sens? Czy footballTeam.ConvertAll <TOutput> () wygląda na coś, czego chcesz?

To nie jest podchwytliwe pytanie; odpowiedź może być naprawdę „tak”. Jeśli zaimplementujesz / odziedziczysz List <Player> lub IList <Player>, utkniesz z nimi; jeśli jest to idealne rozwiązanie dla twojego modelu, zrób to.

Jeśli zdecydujesz się na tak, ma to sens i chcesz, aby Twój obiekt był traktowany jako kolekcja / lista graczy (zachowanie), i dlatego chcesz zaimplementować ICollection lub IList, zrób to. Myślowo:

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

Jeśli chcesz, aby Twój obiekt zawierał kolekcję / listę graczy (danych), a zatem chcesz, aby kolekcja lub lista były własnością lub członkiem, zrób to. Myślowo:

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

Może się wydawać, że chcesz, aby ludzie mogli tylko wyliczyć zestaw graczy, zamiast liczyć ich, dodawać do nich lub usuwać. IEnumerable <Player> jest całkowicie poprawną opcją do rozważenia.

Może się wydawać, że żaden z tych interfejsów w ogóle nie jest użyteczny w twoim modelu. Jest to mniej prawdopodobne (IEnumerable <T> jest przydatne w wielu sytuacjach), ale nadal jest możliwe.

Każdy, kto próbuje powiedzieć ci, że jeden z nich jest kategorycznie i definitywnie niewłaściwy w każdym przypadku, jest wprowadzany w błąd. Każdy, kto próbuje ci to powiedzieć, ma kategoryczne i ostateczne rację w każdym przypadku, jest wprowadzany w błąd.

Przejdź do wdrożenia

Po podjęciu decyzji o danych i zachowaniu możesz podjąć decyzję dotyczącą wdrożenia. Obejmuje to, na których konkretnych klasach polegasz poprzez dziedziczenie lub kompozycję.

To może nie być duży krok, a ludzie często łączą projektowanie i wdrażanie, ponieważ jest całkiem możliwe, aby przejść przez to wszystko w głowie w ciągu sekundy lub dwóch i zacząć pisać.

Eksperyment myślowy

Sztuczny przykład: jak wspomnieli inni, zespół nie zawsze jest „tylko” zbiorem graczy. Czy utrzymujesz zbiór wyników meczów dla drużyny? Czy w twoim modelu drużyna jest wymienna z klubem? Jeśli tak, a jeśli twoja drużyna to zbiór graczy, być może jest to także zbiór pracowników i / lub zbiór wyników. Potem kończysz z:

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

Niezależnie od projektu, w tym momencie w C # i tak nie będzie można zaimplementować wszystkich, dziedzicząc z Listy <T>, ponieważ C # „tylko” obsługuje pojedyncze dziedziczenie. (Jeśli wypróbowałeś tę malarkię w C ++, możesz uznać ją za dobrą rzecz.) Implementacja jednej kolekcji poprzez dziedziczenie, a druga poprzez kompozycję może być brudna. A właściwości, takie jak Count, stają się mylące dla użytkowników, chyba że wyraźnie zaimplementujesz ILIst <Player> .Count i IList <StaffMember> .Count itp., A wtedy będą po prostu bolesne, a nie mylące. Możesz zobaczyć, dokąd to zmierza; przeczucie, że myśląc o tej ścieżce, może ci powiedzieć, że źle jest iść w tym kierunku (i słusznie lub nie, koledzy mogą również, jeśli zastosujesz to w ten sposób!)

Krótka odpowiedź (za późno)

Wytyczna dotycząca nie dziedziczenia z klas kolekcji nie jest specyficzna dla języka C #, znajdziesz ją w wielu językach programowania. Otrzymano mądrość, a nie prawo. Jednym z powodów jest to, że w praktyce uważa się, że skład często wygrywa z dziedziczeniem pod względem zrozumiałości, wykonalności i konserwacji. Znalezienie użytecznych i spójnych relacji „hasa” jest bardziej powszechne w przypadku obiektów świata rzeczywistego / domeny, niż przydatne i spójne relacje „isa”, chyba że zajmujesz się streszczeniem, szczególnie w miarę upływu czasu oraz dokładnych danych i zachowania obiektów w kodzie zmiany. Nie powinno to powodować, że zawsze wykluczasz dziedziczenie po klasach kolekcji; ale może to sugerować.


34

Przede wszystkim dotyczy użyteczności. Jeśli użyjesz dziedziczenia, Teamklasa ujawni zachowanie (metody) zaprojektowane wyłącznie do manipulacji obiektami. Na przykład, AsReadOnly()czy CopyTo(obj)metody nie mają sensu dla obiektu zespołowej. Zamiast AddRange(items)metody prawdopodobnie będziesz potrzebować bardziej opisowej AddPlayers(players)metody.

Jeśli chcesz korzystać z LINQ, wdrożenie bardziej ogólnego interfejsu takiego jak ICollection<T>lub IEnumerable<T>byłoby bardziej sensowne.

Jak wspomniano, kompozycja jest właściwą drogą do tego. Wystarczy zaimplementować listę graczy jako zmienną prywatną.


30

Drużyna piłkarska nie jest listą piłkarzy. Drużyna piłkarska składa się z listy piłkarzy!

To logicznie źle:

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

i to jest poprawne:

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

19
To nie wyjaśnia, dlaczego . OP wie, że większość innych programistów tak się czuje, po prostu nie rozumie, dlaczego jest to ważne. To tylko tak naprawdę zapewnia informacje już zawarte w pytaniu.
Servy

2
To jest pierwsza rzecz, o której myślałem, jak niepoprawnie modelowano jego dane. Więc powodem jest to, że twój model danych jest niepoprawny.
rball

3
Dlaczego nie odziedziczyć z listy? Ponieważ nie chce bardziej szczegółowej listy.
Mauro Sampietro

2
Nie sądzę, aby drugi przykład był poprawny. Odsłaniasz stan, a zatem przerywasz enkapsulację. Stan powinien być ukryty / wewnętrzny w stosunku do obiektu, a sposób mutowania tego stanu powinien odbywać się metodami publicznymi. Dokładnie, które metody należy ustalić na podstawie wymagań biznesowych, ale powinno być to „potrzebuję tego teraz” - podstawa, a nie ”może się przydać jakiś czas nie wiem„ podstawa ”. Trzymaj API ściśle, a zaoszczędzisz czas i wysiłek.
sara

1
Hermetyzacja nie jest sednem pytania. Skupiam się na nie rozszerzaniu typu, jeśli nie chcesz, aby ten typ zachowywał się w bardziej konkretny sposób. Te pola powinny być właściwościami automatycznymi w języku C # i metodami get / set w innych językach, ale tutaj w tym kontekście, który jest całkowicie zbędny
Mauro Sampietro

28

To zależy od kontekstu

Kiedy uważasz swoją drużynę za listę graczy, rzutujesz „pomysł” drużyny piłki nożnej na jeden aspekt: ​​redukujesz „drużynę” do ludzi, których widzisz na boisku. Ta projekcja jest poprawna tylko w określonym kontekście. W innym kontekście może to być całkowicie błędne. Wyobraź sobie, że chcesz zostać sponsorem zespołu. Musisz więc porozmawiać z kierownikami zespołu. W tym kontekście zespół jest rzutowany na listę swoich menedżerów. Te dwie listy zwykle nie nakładają się zbytnio. Inne konteksty są obecne w porównaniu do poprzednich graczy itp.

Niejasna semantyka

Problem z uznaniem zespołu za listę jego graczy polega na tym, że jego semantyczny zależy od kontekstu i że nie można go rozszerzyć, gdy zmienia się kontekst. Ponadto trudno jest wyrazić, którego kontekstu używasz.

Zajęcia są rozszerzalne

Gdy używasz klasy z tylko jednym członkiem (np. IList activePlayers), Możesz użyć nazwy członka (i dodatkowo jego komentarza), aby wyjaśnić kontekst. Gdy istnieją dodatkowe konteksty, wystarczy dodać dodatkowego członka.

Zajęcia są bardziej złożone

W niektórych przypadkach utworzenie dodatkowej klasy może być przesadą. Każda definicja klasy musi zostać załadowana przez moduł ładujący klasy i będzie buforowana przez maszynę wirtualną. To kosztuje wydajność i pamięć w czasie wykonywania. Gdy masz bardzo specyficzny kontekst, uważanie drużyny piłkarskiej za listę graczy może być w porządku. Ale w tym przypadku powinieneś po prostu użyć IList klasy, a nie pochodnej od niej klasy.

Wnioski / uwagi

Kiedy masz bardzo specyficzny kontekst, możesz uważać drużynę za listę graczy. Na przykład wewnątrz metody napisanie:

IList<Player> footballTeam = ...

Podczas korzystania z F # może być nawet OK utworzenie skrótu typu:

type FootballTeam = IList<Player>

Ale kiedy kontekst jest szerszy lub nawet niejasny, nie powinieneś tego robić. Dzieje się tak zwłaszcza w przypadku tworzenia nowej klasy, której kontekst, w którym może być używana w przyszłości, nie jest jasny. Znak ostrzegawczy pojawia się, gdy zaczynasz dodawać dodatkowe atrybuty do swojej klasy (nazwa drużyny, trenera itp.). Jest to wyraźny znak, że kontekst, w którym klasa będzie używana, nie jest ustalony i zmieni się w przyszłości. W takim przypadku nie można traktować drużyny jako listy graczy, ale należy modelować listę graczy (aktualnie aktywnych, nie rannych itp.) Jako atrybut drużyny.


27

Pozwól mi przepisać twoje pytanie. więc możesz zobaczyć ten temat z innej perspektywy.

Kiedy muszę reprezentować drużynę piłkarską, rozumiem, że to w zasadzie nazwa. Jak: „Orły”

string team = new string();

Potem zdałem sobie sprawę, że drużyny też mają graczy.

Dlaczego nie mogę po prostu rozszerzyć typu łańcucha, aby zawierał on także listę graczy?

Twój punkt wejścia w problem jest arbitralny. Spróbuj pomyśleć, co zespół ma (właściwości), a nie co to jest .

Po wykonaniu tej czynności możesz sprawdzić, czy dzieli właściwości z innymi klasami. I pomyśl o dziedziczeniu.


1
To dobra uwaga - można by pomyśleć o drużynie jako o nazwie. Jeśli jednak moja aplikacja chce pracować z działaniami graczy, to myślenie jest nieco mniej oczywiste. W każdym razie wydaje się, że kwestia sprowadza się w końcu do kompozycji vs.
Superbest

6
Jest to jeden z godnych naśladowania sposobów. Na marginesie, proszę wziąć pod uwagę: bogaty człowiek ma kilka drużyn piłkarskich, daje jeden przyjacielowi w prezencie, przyjaciel zmienia nazwę drużyny, zwalnia trenera, i zastępuje wszystkich graczy. Drużyna przyjaciół spotyka drużynę mężczyzn na zielonym polu, a gdy drużyna mężczyzn przegrywa, mówi: „Nie mogę uwierzyć, że bijesz mnie drużyną, którą ci dałem!” czy mężczyzna ma rację? jak byś to sprawdził?
user1852503

20

Tylko dlatego, że myślę, że inne odpowiedzi w dużej mierze idą w parze z tym, czy drużyna piłkarska „jest” List<FootballPlayer>czy „ma” List<FootballPlayer>, co tak naprawdę nie odpowiada na to pytanie w formie pisemnej.

PO głównie prosi o wyjaśnienie wytycznych dotyczących dziedziczenia po List<T>:

Wytyczne mówią, że nie powinieneś dziedziczyć List<T>. Dlaczego nie?

Ponieważ List<T>nie ma wirtualnych metod. Jest to mniejszy problem w twoim własnym kodzie, ponieważ zwykle możesz zrezygnować z implementacji przy stosunkowo niewielkim bólu - ale może to być znacznie większa sprawa w publicznym API.

Co to jest publiczny interfejs API i dlaczego powinienem się tym przejmować?

Publiczny interfejs API to interfejs udostępniany programistom zewnętrznym. Pomyśl kod frameworka. I pamiętaj, że wspomniane wytyczne to „ Wytyczne dotyczące projektowania .NET Framework ”, a nie „ Wytyczne dotyczące projektowania aplikacji .NET ”. Jest różnica i - ogólnie mówiąc - publiczny projekt API jest znacznie bardziej rygorystyczny.

Jeśli mój obecny projekt nie ma i prawdopodobnie nie będzie miał tego publicznego interfejsu API, czy mogę bezpiecznie zignorować tę wytyczną? Jeśli odziedziczę po liście i okaże się, że potrzebuję publicznego interfejsu API, jakie będę miał trudności?

Prawie tak. Możesz zastanowić się nad uzasadnieniem, aby sprawdzić, czy i tak odnosi się ono do twojej sytuacji, ale jeśli nie budujesz publicznego interfejsu API, nie musisz szczególnie martwić się o problemy z interfejsem API, takie jak wersjonowanie (którego jest to podzbiór).

Jeśli dodasz publiczny interfejs API w przyszłości, będziesz musiał albo wyodrębnić interfejs API z implementacji (nie ujawniając List<T>bezpośrednio), albo naruszyć wytyczne, co może się wiązać z przyszłym bólem.

Dlaczego to w ogóle ma znaczenie? Lista jest listą. Co może się zmienić? Co mógłbym chcieć zmienić?

Zależy od kontekstu, ale skoro używamy FootballTeamjako przykładu - wyobraź sobie, że nie możesz dodać, FootballPlayerjeśli spowodowałoby to przekroczenie pułapu wynagrodzenia przez zespół. Możliwym sposobem dodania tego byłoby coś takiego:

 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!");
        }
     }
 }

Ach ... ale nie możesz, override Addbo to nie jest virtual(ze względu na wydajność).

Jeśli jesteś w aplikacji (co w zasadzie oznacza, że ​​ty i wszyscy twoi rozmówcy są kompilowani razem), możesz teraz przejść na używanie IList<T>i naprawiać wszelkie błędy kompilacji:

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

ale jeśli publicznie ujawniłeś się innej firmie, dokonałeś przełomowej zmiany, która spowoduje błędy kompilacji i / lub działania.

TL; DR - wytyczne dotyczą publicznych interfejsów API. W przypadku prywatnych interfejsów API rób, co chcesz.


18

Czy pozwala ludziom mówić

myTeam.subList(3, 5);

ma jakiś sens? Jeśli nie, to nie powinna to być lista.


3
Może by tak nazwaćmyTeam.subTeam(3, 5);
Sam Leach

3
@SamLeach Zakładając, że to prawda, nadal będziesz potrzebować kompozycji, a nie dziedziczenia. Jak subListjuż nawet nie powróci Team.
Cruncher

1
To przekonało mnie bardziej niż jakakolwiek inna odpowiedź. +1
FireCubez

16

Zależy to od zachowania obiektu „zespołowego”. Jeśli zachowuje się jak kolekcja, może być dobrze, aby najpierw go przedstawić za pomocą zwykłej listy. Wtedy możesz zacząć zauważać, że ciągle powielasz kod, który iteruje na liście; w tym momencie masz możliwość utworzenia obiektu FootballTeam, który otacza listę graczy. Klasa FootballTeam staje się domem dla całego kodu, który iteruje na liście graczy.

To sprawia, że ​​mój kod jest niepotrzebnie pełny. Teraz muszę zadzwonić do my_team.Players.Count zamiast tylko my_team.Count. Na szczęście za pomocą C # mogę zdefiniować indeksatory, aby indeksowanie było przejrzyste i przesłać dalej wszystkie metody z wewnętrznej listy ... Ale to dużo kodu! Co dostanę za całą tę pracę?

Kapsułkowanie. Twoi klienci nie muszą wiedzieć, co dzieje się w FootballTeam. Wszyscy Twoi klienci wiedzą, że można go wdrożyć, przeglądając listę graczy w bazie danych. Nie muszą wiedzieć, a to poprawia twój projekt.

To po prostu nie ma sensu. Drużyna futbolowa nie ma „listy graczy”. To jest lista graczy. Nie mówisz: „John McFootballer dołączył do graczy SomeTeam”. Mówisz „John dołączył do SomeTeam”. Nie dodajesz litery do „znaków łańcucha”, dodajesz literę do łańcucha. Nie dodajesz książki do książek w bibliotece, dodajesz książkę do biblioteki.

Dokładnie :) powiesz footballTeam.Add (John), a nie footballTeam.List.Add (John). Wewnętrzna lista nie będzie widoczna.


1
OP chce zrozumieć, jak MODELOWAĆ RZECZYWISTOŚĆ, ale następnie definiuje drużynę piłkarską jako listę piłkarzy (co jest błędne koncepcyjnie). To jest problem. Moim zdaniem każdy inny argument wprowadza go w błąd.
Mauro Sampietro

3
Nie zgadzam się, że lista graczy jest błędna koncepcyjnie. Niekoniecznie. Jak napisał ktoś na tej stronie: „Wszystkie modele są złe, ale niektóre są przydatne”
xpmatteo,

Właściwe modele są trudne do zdobycia, po wykonaniu są one przydatne. Jeżeli model może być niewłaściwie używany, jest błędny koncepcyjnie. stackoverflow.com/a/21706411/711061
Mauro Sampietro

15

Jest tu wiele doskonałych odpowiedzi, ale chcę dotknąć czegoś, o czym nie wspomniałem: Projektowanie obiektowe polega na wzmacnianiu obiektów .

Chcesz zawrzeć wszystkie swoje reguły, dodatkowe prace i szczegóły wewnętrzne w odpowiednim obiekcie. W ten sposób inne obiekty wchodzące w interakcje z tym jednym nie muszą się o to martwić. W rzeczywistości chcesz pójść o krok dalej i aktywnie zapobiegać omijaniu tych obiektów przez inne obiekty.

Kiedy odziedziczysz po List, wszystkie inne obiekty będą cię widzieć jako listę. Mają bezpośredni dostęp do metod dodawania i usuwania graczy. I stracisz kontrolę; na przykład:

Załóżmy, że chcesz rozróżnić, kiedy gracz wychodzi, wiedząc, czy przeszedł na emeryturę, zrezygnował czy został zwolniony. Można zaimplementować RemovePlayermetodę, która przyjmuje odpowiedni wyliczenie wejściowe. Jednak dziedzicząc po List, nie można uniemożliwić bezpośredniego dostępu Remove, RemoveAlla nawet Clear. W rezultacie faktycznie osłabiłeś swoją FootballTeamklasę.


Dodatkowe przemyślenia na temat enkapsulacji ... Podniosłeś następujące obawy:

To sprawia, że ​​mój kod jest niepotrzebnie pełny. Teraz muszę zadzwonić do my_team.Players.Count zamiast tylko my_team.Count.

Masz rację, to byłoby niepotrzebnie gadatliwe dla wszystkich klientów, którzy mogliby korzystać z twojego zespołu. Jednak ten problem jest bardzo niewielki w porównaniu z faktem, że wystawiłeś się List Playersna wszystko i różne, aby mogli bawić się z twoim zespołem bez twojej zgody.

Dalej mówisz:

To po prostu nie ma sensu. Drużyna futbolowa nie ma „listy graczy”. To jest lista graczy. Nie mówisz: „John McFootballer dołączył do graczy SomeTeam”. Mówisz „John dołączył do SomeTeam”.

Mylisz się co do pierwszej rzeczy: upuść słowo „lista”, a właściwie to oczywiste, że drużyna ma graczy.
Jednak uderzasz gwoździem w głowę drugim. Nie chcesz, żeby klienci dzwonili ateam.Players.Add(...). Chcesz, żeby dzwonili ateam.AddPlayer(...). Twoja implementacja zadzwoniłaby (być może między innymi) Players.Add(...)wewnętrznie.


Mam nadzieję, że zobaczysz, jak ważne jest kapsułkowanie w celu wzmocnienia swoich obiektów. Chcesz, aby każda klasa dobrze wykonywała swoją pracę bez obawy o ingerencję innych obiektów.


12

Jaki jest prawidłowy sposób C # reprezentowania struktury danych ...

Pamiętaj: „Wszystkie modele są złe, ale niektóre są przydatne”. - George EP Box

Nie ma „właściwej drogi”, tylko pożytecznej.

Wybierz taki, który będzie użyteczny dla Ciebie i / lub Twoich użytkowników. Otóż ​​to. Rozwijaj się ekonomicznie, nie nadużywaj inżynierii. Im mniej kodu napiszesz, tym mniej kodu będziesz potrzebować do debugowania. (przeczytaj następujące wydania).

- Edytowane

Moja najlepsza odpowiedź brzmiałaby ... to zależy. Dziedziczenie z listy naraziłoby klientów tej klasy na metody, które mogą nie być narażone, przede wszystkim dlatego, że FootballTeam wygląda jak jednostka biznesowa.

- Edycja 2

Szczerze nie pamiętam, o czym mówiłem w komentarzu „nie przesadzaj z inżynierią”. Chociaż uważam, że sposób myślenia KISS jest dobrym przewodnikiem, chcę podkreślić, że dziedziczenie klasy biznesowej z Listu stworzyłoby więcej problemów niż rozwiązuje, z powodu wycieku abstrakcji .

Z drugiej strony uważam, że istnieje ograniczona liczba przypadków, w których po prostu dziedziczenie z Listy jest przydatne. Jak napisałem w poprzednim wydaniu, to zależy. Na odpowiedź na każdy przypadek duży wpływ ma zarówno wiedza, doświadczenie, jak i osobiste preferencje.

Dzięki @kai za pomoc w dokładniejszym zastanowieniu się nad odpowiedzią.


Moja najlepsza odpowiedź brzmiałaby ... to zależy. Dziedziczenie z listy naraziłoby klientów tej klasy na metody, które mogą nie być narażone, przede wszystkim dlatego, że FootballTeam wygląda jak jednostka biznesowa. Nie wiem, czy powinienem edytować tę odpowiedź, czy opublikować inną, jakiś pomysł?
ArturoTena

2
Trochę o „ dziedziczeniu po a Listnaraziłoby klientów tej klasy na metody, które mogą nie być narażone ”, właśnie to powoduje, że dziedziczenie po Listabsolutnym nie-nie. Po ujawnieniu, z czasem stają się one wykorzystywane i niewłaściwie wykorzystywane. Oszczędność czasu dzięki „rozwojowi ekonomicznemu” może z łatwością doprowadzić do dziesięciokrotności oszczędności utraconych w przyszłości: debugowania nadużyć i ostatecznie refaktoryzacji w celu naprawienia spadku. Zasada YAGNI może być również uważana za znaczącą: nie potrzebujesz wszystkich tych metod List, więc nie ujawniaj ich.
Rozczarowany

3
„nie nadużywaj inżynierii. Im mniej kodu napiszesz, tym mniej kodu będziesz potrzebować do debugowania”. <- Myślę, że to wprowadza w błąd. Hermetyzacja i kompozycja nad dziedziczeniem NIE jest nadmierną inżynierią i nie powoduje większej potrzeby debugowania. Podsumowując, OGRANICZAJ liczbę sposobów, w jakie klienci mogą wykorzystywać (i nadużywać) twoją klasę, a zatem masz mniej punktów wejścia, które wymagają testowania i sprawdzania poprawności danych wejściowych. Dziedziczenie po tym, Listponieważ jest szybkie i łatwe, a tym samym doprowadziłoby do mniejszej liczby błędów, jest po prostu błędne, to po prostu zły projekt, a zły projekt prowadzi do znacznie większej liczby błędów niż „nadmierna inżynieria”.
sara,

@ kai Zgadzam się z tobą w każdym punkcie. Szczerze nie pamiętam, o czym mówiłem w komentarzu „nie przesadzaj z inżynierią”. OTOH, uważam, że istnieje ograniczona liczba przypadków, w których po prostu dziedziczenie z Listy jest przydatne. Jak napisałem w późniejszym wydaniu, to zależy. Na odpowiedź na każdy przypadek duży wpływ ma zarówno wiedza, doświadczenie, jak i osobiste preferencje. Jak wszystko w życiu. ;-)
ArturoTena

11

To przypomina mi, że „jest kontra” ma „kompromis”. Czasami łatwiej i bardziej sensowne jest dziedziczenie bezpośrednio od super klasy. Innym razem bardziej sensowne jest utworzenie samodzielnej klasy i uwzględnienie klasy, którą odziedziczyłbyś jako zmienną składową. Nadal możesz uzyskać dostęp do funkcjonalności klasy, ale nie jest związany z interfejsem ani innymi ograniczeniami, które mogą wynikać z dziedziczenia po klasie.

Co robisz Podobnie jak w przypadku wielu rzeczy ... zależy to od kontekstu. Poradnik, którego użyłbym, jest taki, że aby odziedziczyć po innej klasie, naprawdę powinna istnieć relacja „jest”. Więc jeśli piszesz klasę o nazwie BMW, może odziedziczyć po Car, ponieważ BMW naprawdę jest samochodem. Klasa koni może odziedziczyć po klasie ssaków, ponieważ w rzeczywistości koń jest ssakem, a każda funkcjonalność ssaków powinna być odpowiednia dla konia. Ale czy możesz powiedzieć, że zespół to lista? Z tego, co mogę powiedzieć, nie wydaje się, że zespół naprawdę jest „listą”. W tym przypadku miałbym List jako zmienną składową.


9

Mój brudny sekret: nie dbam o to, co mówią ludzie i robię to. .NET Framework jest rozpowszechniany za pomocą „XxxxCollection” (na przykład UIElementCollection).

Co powstrzymuje mnie od powiedzenia:

team.Players.ByName("Nicolas")

Kiedy znajdę to lepiej niż

team.ByName("Nicolas")

Co więcej, moja PlayerCollection może być używana przez inne klasy, takie jak „Club” bez powielania kodu.

club.Players.ByName("Nicolas")

Najlepsze praktyki z wczoraj mogą nie być jutrzejsze. Większość najlepszych praktyk nie ma powodu, większość jest tylko szeroką zgodą społeczności. Zamiast pytać społeczność, czy będzie cię winić, kiedy to zrobisz, zadaj sobie pytanie, co jest bardziej czytelne i łatwe do utrzymania?

team.Players.ByName("Nicolas") 

lub

team.ByName("Nicolas")

Naprawdę. Czy masz jakieś wątpliwości? Być może teraz musisz grać z innymi ograniczeniami technicznymi, które uniemożliwiają korzystanie z Listy <T> w prawdziwym przypadku użycia. Ale nie dodawaj ograniczeń, które nie powinny istnieć. Jeśli Microsoft nie udokumentował, dlaczego, to z pewnością jest to „najlepsza praktyka” pochodząca znikąd.


9
Chociaż ważna jest odwaga i inicjatywa, by zakwestionować przyjętą mądrość w stosownych przypadkach, uważam, że rozsądnie jest najpierw zrozumieć, dlaczego przyjęta mądrość została przyjęta przede wszystkim, zanim zacznie ją kwestionować. -1, ponieważ „Zignoruj ​​tę wskazówkę!” nie jest dobrą odpowiedzią na „Dlaczego ta wytyczna istnieje?” Nawiasem mówiąc, społeczność nie tylko „winiła mnie” w tym przypadku, ale dostarczyła satysfakcjonującego wyjaśnienia, które przekonało mnie.
Superbest

3
W rzeczywistości, kiedy nie ma wyjaśnienia, jedynym sposobem jest przekroczenie granicy i przetestowanie, nie bojąc się naruszenia. Teraz. Zadaj sobie pytanie, nawet jeśli Lista <T> nie była dobrym pomysłem. Jak trudny byłaby zmiana dziedziczenia z listy <T> na kolekcję <T>? Zgadnij: mniej czasu niż publikowanie na forum, bez względu na długość kodu z narzędziami do refaktoryzacji. To dobre pytanie, ale nie praktyczne.
Nicolas Dorier

Aby wyjaśnić: z pewnością nie czekam na błogosławieństwo SO, aby odziedziczyć po tym, czego chcę, jednak chcę tego, ale celem mojego pytania było zrozumienie rozważań, które należy podjąć w tej decyzji i dlaczego tak wielu doświadczonych programistów wydaje się mieć zdecydowałem odwrotnie niż to, co zrobiłem. Collection<T>to nie to samo, co List<T>może wymagać sporo pracy, aby zweryfikować w dużym projekcie.
Superbest

2
team.ByName("Nicolas")oznacza "Nicolas"nazwę zespołu.
Sam Leach

1
„nie dodawaj ograniczeń, które nie powinny istnieć” Au contraire, nie wystawiaj niczego, czego nie masz uzasadnionego powodu. To nie jest puryzm ani ślepa gorliwość, ani nie jest to najnowszy trend w modzie projektowej. Jest to podstawowy obiektowy projekt 101, poziom dla początkujących. Nie jest tajemnicą, dlaczego ta „reguła” istnieje, jest ona wynikiem kilkudziesięciu lat doświadczenia.
sara

8

Wytyczne mówią, że publiczny interfejs API nie powinien ujawniać wewnętrznej decyzji projektowej dotyczącej tego, czy korzystasz z listy, zestawu, słownika, drzewa, czy cokolwiek innego. „Zespół” niekoniecznie musi być listą. Możesz zaimplementować go jako listę, ale użytkownicy twojego publicznego interfejsu API powinni używać twojej klasy w oparciu o wiedzę. Umożliwia to zmianę decyzji i użycie innej struktury danych bez wpływu na interfejs publiczny.


Z perspektywy czasu, po wyjaśnieniu @EricLippert i innych, faktycznie udzieliłeś świetnej odpowiedzi na część API mojego pytania - oprócz tego, co powiedziałeś, jeśli to zrobię class FootballTeam : List<FootballPlayers>, użytkownicy mojej klasy będą mogli powiedzieć: odziedziczyłem List<T>po zastosowaniu refleksji, dostrzeganiu List<T>metod, które nie mają sensu dla drużyny piłkarskiej, i możliwości korzystania FootballTeamz nich List<T>, więc ujawniłem klientowi szczegóły implementacji (niepotrzebnie).
Superbest

7

Kiedy mówią, że List<T>jest „zoptymalizowany”, myślę, że chcą oznaczać, że nie ma funkcji takich jak metody wirtualne, które są nieco droższe. Problem polega na tym, że po ujawnieniu List<T>w publicznym interfejsie API tracisz możliwość egzekwowania reguł biznesowych lub późniejszego dostosowania jego funkcjonalności. Ale jeśli używasz tej odziedziczonej klasy jako wewnętrznej w swoim projekcie (w przeciwieństwie do potencjalnie narażonej na tysiące twoich klientów / partnerów / innych zespołów jako API), może być OK, jeśli oszczędza Twój czas i jest to funkcja, którą chcesz duplikować. Zaletą dziedziczenia List<T>jest to, że eliminujesz dużo głupiego kodu opakowania, którego po prostu nigdy nie będziesz dostosowywać w dającej się przewidzieć przyszłości. Także jeśli chcesz, aby twoja klasa miała dokładnie taką samą semantykę jakList<T> przez cały okres użytkowania interfejsów API również może być OK.

Często widzę wiele osób wykonujących mnóstwo dodatkowej pracy tylko z powodu reguły FxCop lub ktoś na blogu twierdzi, że jest to „zła” praktyka. Wiele razy zamienia to kod w projektowanie dziwnego wzoru palooza. Podobnie jak w przypadku wielu wytycznych, należy traktować je jako wytyczne, które mogą mieć wyjątki.


4

Chociaż nie mam złożonego porównania, jak większość tych odpowiedzi, chciałbym podzielić się moją metodą radzenia sobie z tą sytuacją. Rozszerzając IEnumerable<T>, możesz pozwolić swojej Teamklasie na obsługę rozszerzeń zapytań Linq, bez publicznego ujawniania wszystkich metod i właściwości 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

Jeśli użytkownicy twojej klasy potrzebują wszystkich metod i właściwości ** Lista ma, powinieneś z niej wyprowadzić klasę. Jeśli nie potrzebują ich, dołącz Listę i utwórz opakowania dla metod, których faktycznie potrzebują użytkownicy twojej klasy.

Jest to ścisła zasada, jeśli piszesz publiczny interfejs API lub inny kod, który będzie używany przez wiele osób. Możesz zignorować tę zasadę, jeśli masz małą aplikację i nie więcej niż 2 programistów. Zaoszczędzi ci to trochę czasu.

W przypadku niewielkich aplikacji możesz również rozważyć wybór innego, mniej rygorystycznego języka. Ruby, JavaScript - wszystko, co pozwala napisać mniej kodu.


3

Chciałem tylko dodać, że Bertrand Meyer, wynalazca Eiffla i projektu na podstawie umowy, Teamodziedziczyłby po List<Player>tym, jak mrugnąłby powieką.

W swojej książce Object-Oriented Software Construction omawia implementację systemu GUI, w którym okna prostokątne mogą mieć okna potomne. Po prostu Windowodziedziczył oba te elementy Rectanglei Tree<Window>ponownie wykorzystał implementację.

Jednak C # to nie Eiffel. Ten ostatni obsługuje wielokrotne dziedziczenie i zmianę nazw funkcji . W języku C #, gdy podklasujesz, dziedziczysz zarówno interfejs, jak i implementację. Możesz zastąpić implementację, ale konwencje wywoływania są kopiowane bezpośrednio z nadklasy. W Eiffel, jednak można modyfikować nazwy metod publicznych, dzięki czemu można zmieniać nazwy Addi Removedo Hirei Firew swojej Team. Jeśli instancja Teamjest uskok z powrotem List<Player>, rozmówca będzie korzystał Addi Removedo niego, ale swoje wirtualne metody modyfikacji Hirei Firebędzie nazwany.


1
W tym przypadku problemem nie jest wielokrotne dziedziczenie, miksy (itp.) Ani sposób radzenia sobie z ich brakiem. W każdym języku, nawet w języku ludzkim, zespół nie jest listą osób, ale składa się z listy osób. To jest abstrakcyjna koncepcja. Jeśli Bertrand Meyer lub ktoś zarządza zespołem przez podklasowanie listy, robi to źle. Powinieneś podklasować listę, jeśli chcesz mieć bardziej szczegółowy rodzaj listy. Mam nadzieję, że się zgadzasz.
Mauro Sampietro,

1
To zależy od tego, jakie jest dla ciebie dziedzictwo. Samo w sobie jest to operacja abstrakcyjna, nie oznacza to, że dwie klasy są w „szczególnym rodzaju” relacji, mimo że jest to interpretacja głównego nurtu. Dziedziczenie można jednak traktować jako mechanizm ponownego wykorzystania implementacji, jeśli projekt języka go obsługuje, nawet jeśli kompozycja jest obecnie preferowaną alternatywą.
Alexey,

2

Myślę, że nie zgadzam się z twoim uogólnieniem. Zespół to nie tylko zbiór graczy. Zespół ma o wiele więcej informacji na ten temat - nazwisko, godło, zbiór kadry zarządzającej / administracyjnej, zbiór ekipy trenerskiej, a następnie zbiór graczy. Tak właściwie klasa FootballTeam powinna mieć 3 kolekcje, a nie sama w sobie; jeśli ma właściwie modelować rzeczywisty świat.

Możesz rozważyć klasę PlayerCollection, która podobnie jak Specialized StringCollection oferuje inne funkcje - takie jak sprawdzanie poprawności i sprawdzanie, zanim obiekty zostaną dodane do magazynu wewnętrznego lub usunięte z niego.

Być może pojęcie gracza PlayerCollection pasuje do Twojego preferowanego podejścia?

public class PlayerCollection : Collection<Player> 
{ 
}

A następnie FootballTeam może wyglądać następująco:

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

Wolę interfejsy niż klasy

Klasy powinny unikać wywodzenia się z klas i zamiast tego implementować minimalne niezbędne interfejsy.

Dziedziczenie przerywa enkapsulację

Pochodzenie z klas przerywa enkapsulację :

  • ujawnia wewnętrzne szczegóły dotyczące sposobu wdrożenia Twojej kolekcji
  • deklaruje interfejs (zestaw funkcji publicznych i właściwości), który może nie być odpowiedni

Między innymi utrudnia to refaktoryzację kodu.

Klasy są szczegółami implementacji

Klasy są szczegółami implementacji, które powinny być ukryte przed innymi częściami twojego kodu. W skrócieSystem.List jest konkretną implementacją abstrakcyjnego typu danych, która może, ale nie musi być odpowiednia teraz i w przyszłości.

Pod względem koncepcyjnym fakt, że System.Listtyp danych nazywa się „listą”, jest nieco czerwony. A System.List<T>jest zmienną uporządkowaną kolekcją, która obsługuje zamortyzowane operacje O (1) do dodawania, wstawiania i usuwania elementów oraz operacje O (1) do pobierania liczby elementów lub pobierania i ustawiania elementu według indeksu.

Im mniejszy interfejs, tym bardziej elastyczny kod

Podczas projektowania struktury danych, im prostszy jest interfejs, tym bardziej elastyczny jest kod. Wystarczy spojrzeć na to, jak potężny jest LINQ do demonstracji tego.

Jak wybrać interfejsy

Kiedy myślisz „lista”, powinieneś zacząć od powiedzenia sobie: „Muszę reprezentować kolekcję graczy w baseball”. Powiedzmy, że zdecydujesz się na modelowanie tego z klasą. To, co powinieneś najpierw zrobić, to zdecydować, jaką minimalną liczbę interfejsów będzie musiała ujawnić ta klasa.

Niektóre pytania, które mogą pomóc w przeprowadzeniu tego procesu:

  • Czy muszę mieć rachubę? Jeśli nie, rozważ wdrożenieIEnumerable<T>
  • Czy ta kolekcja zmieni się po zainicjowaniu? Jeśli nie rozważ IReadonlyList<T>.
  • Czy ważne jest, że mogę uzyskać dostęp do przedmiotów według indeksu? RozważaćICollection<T>
  • Czy kolejność dodawania elementów do kolekcji jest ważna? Może to jest ISet<T>?
  • Jeśli naprawdę chcesz tych rzeczy, to idź dalej IList<T>.

W ten sposób nie będziesz łączyć innych części kodu ze szczegółami implementacji swojej kolekcji graczy w baseball i będziesz mógł dowolnie zmieniać sposób jego implementacji, pod warunkiem przestrzegania interfejsu.

Stosując to podejście, przekonasz się, że kod staje się łatwiejszy do odczytania, refaktoryzacji i ponownego użycia.

Uwagi na temat unikania płyty kotłowej

Implementacja interfejsów w nowoczesnym środowisku IDE powinna być łatwa. Kliknij prawym przyciskiem myszy i wybierz „Implementuj interfejs”. Następnie przekaż wszystkie implementacje do klasy członka, jeśli zajdzie taka potrzeba.

To powiedziawszy, jeśli okaże się, że piszesz dużo bojlerów, to potencjalnie dlatego, że udostępniasz więcej funkcji, niż powinieneś. Jest to ten sam powód, dla którego nie powinieneś dziedziczyć po klasie.

Możesz także zaprojektować mniejsze interfejsy, które mają sens dla twojej aplikacji, a może tylko kilka funkcji rozszerzających pomocnika, aby odwzorować te interfejsy na inne, których potrzebujesz. Takie podejście zastosowałem we własnym IArrayinterfejsie dla biblioteki LinqArray .


2

Problemy z serializacją

Brakuje jednego aspektu. Klasy dziedziczące z List <> nie mogą być poprawnie serializowane przy użyciu XmlSerializer . W takim przypadku należy użyć DataContractSerializer lub potrzebna jest własna implementacja serializacji.

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

Dalsza lektura: Gdy klasa jest dziedziczona z List <>, XmlSerializer nie serializuje innych atrybutów


0

Kiedy to jest dopuszczalne?

Cytując Erica Lipperta:

Kiedy budujesz mechanizm, który rozszerza ten List<T>mechanizm.

Na przykład jesteś zmęczony brakiem AddRangemetody w IList<T>:

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

public class MoreConvenientList<T> : List<T>, IMoreConvenientListInterface<T> { }
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.