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, string
nazwa 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 (anint
). .NET nie zapewnia klasy do przechowywania drużyn piłkarskich, więc stworzę własną klasę. Najbardziej podobna i odpowiednia istniejąca struktura jestList<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 List
jest 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 List
zapewnia 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ł Collection
klasę, 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 AddRange
nie jest lepsze niż brak AddRange
?
Dziedziczenie po Collection
to dużo więcej pracy niż dziedziczenie List
i 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 Collection
nie 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 List
czegoś w:
class FootballTeam
{
public List<FootballPlayer> Players;
}
Są z tym dwa problemy:
To sprawia, że mój kod jest niepotrzebnie pełny. Muszę teraz zadzwonić
my_team.Players.Count
zamiast po prostumy_team.Count
. Na szczęście za pomocą C # mogę zdefiniować indeksatory, aby indeksowanie było przejrzyste i przekazywać wszystkie metody wewnętrzneList
... Ale to dużo kodu! Co dostanę za całą tę pracę?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 list
z things
kilkoma 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?
string
jest wymagane, aby zrobić wszystko, coobject
może zrobić i więcej .Odpowiedzi:
Tutaj jest kilka dobrych odpowiedzi. Dodałbym do nich następujące punkty.
Poproś dowolne dziesięć osób niebędących programistami komputerowymi, które są obeznane z istnieniem futbolu, o wypełnienie pustego pola:
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 nazwieRoster
toList<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.Nie do przyjęcia dla kogo? Mnie? Nie.
Kiedy budujesz mechanizm, który rozszerza ten
List<T>
mechanizm .Czy buduję mechanizm lub obiekt biznesowy ?
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.
źródło
IEnumerable<T>
- sekwencja ?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 wszystkiego
List<T>
List<T>
jest wysoce zoptymalizowany. Jego główne zastosowanie ma być używane jako prywatny członek obiektu.class MyList<T, TX> : List<CustomObject<T, Something<TX>> { ... }
. Teraz jest to tak proste jak robienievar list = new MyList<int, string>();
.IList<T>
, jakbyś potrzebował dowolnego konsumenta, aby mieć listę indeksowaną. To pozwoli ci później zmienić implementację w klasie.Collection<T>
bardzo ogólny, ponieważ jest to ogólna koncepcja ... nazwa mówi wszystko; to tylko kolekcja. Są bardziej precyzyjne wersje takie jakSortedCollection<T>
,ObservableCollection<T>
,ReadOnlyCollection<T>
, itd., Z których każdy wdrażająIList<T>
ale nieList<T>
.Collection<T>
pozwala na zastąpienie członków (tj. Dodaj, Usuń itp.), ponieważ są wirtualni.List<T>
nie.Gdybym pisał ten kod, klasa prawdopodobnie wyglądałaby mniej więcej tak:
źródło
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 jedynieclass 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ówList<T>
.Collection<T> allows for members (i.e. Add, Remove, etc.) to be overridden
Teraz , że jest to dobry powód, aby nie dziedziczyć z listy. Pozostałe odpowiedzi mają zbyt wiele filozofii.Poprzedni kod oznacza: grupa facetów z ulicy grających w piłkę nożną, a oni mają imię. Coś jak:
W każdym razie ten kod (z mojej odpowiedzi)
Oznacza: to drużyna piłkarska, która ma kierownictwo, zawodników, administratorów itp. Coś w stylu:
Oto jak twoja logika jest przedstawiona na zdjęciach…
źródło
private readonly List<T> _roster = new List<T>(44);
System.Linq
przestrzeni nazw.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:
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.
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.
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.
źródło
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:
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.
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).
źródło
Co jeśli
FootballTeam
zespół rezerwowy wraz z zespołem głównym?Jak byś to modelował?
Związek jest wyraźnie , a nie jest .
czy
RetiredPlayers
?Zasadniczo, jeśli chcesz dziedziczyć po kolekcji, nazwij klasę
SomethingCollection
.Czy twój
SomethingCollection
semantycznie ma sens? Zrób to tylko, jeśli Twój typ to zbiórSomething
.W przypadku
FootballTeam
tego nie brzmi dobrze.Team
Jest ponadCollection
. ZATeam
Moż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 dziedziczyList<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
Czy ? lub Has ?
X
Y
X
Y
Czy moje nazwy klas oznaczają, jakie są?
źródło
DefensivePlayers
,OffensivePlayers
alboOtherPlayers
, ż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ówDefensivePlayers
,OffsensivePlayers
lubSpecialPlayers
typuIList<DefensivePlayer>
,IList<OffensivePlayer>
orazIList<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 ...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ć:
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:
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:
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:
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ć.
źródło
Przede wszystkim dotyczy użyteczności. Jeśli użyjesz dziedziczenia,
Team
klasa ujawni zachowanie (metody) zaprojektowane wyłącznie do manipulacji obiektami. Na przykład,AsReadOnly()
czyCopyTo(obj)
metody nie mają sensu dla obiektu zespołowej. ZamiastAddRange(items)
metody prawdopodobnie będziesz potrzebować bardziej opisowejAddPlayers(players)
metody.Jeśli chcesz korzystać z LINQ, wdrożenie bardziej ogólnego interfejsu takiego jak
ICollection<T>
lubIEnumerable<T>
byłoby bardziej sensowne.Jak wspomniano, kompozycja jest właściwą drogą do tego. Wystarczy zaimplementować listę graczy jako zmienną prywatną.
źródło
Drużyna piłkarska nie jest listą piłkarzy. Drużyna piłkarska składa się z listy piłkarzy!
To logicznie źle:
i to jest poprawne:
źródło
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:
Podczas korzystania z F # może być nawet OK utworzenie skrótu typu:
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.
źródło
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”
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.
źródło
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>
: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.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.
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.Zależy od kontekstu, ale skoro używamy
FootballTeam
jako przykładu - wyobraź sobie, że nie możesz dodać,FootballPlayer
jeśli spowodowałoby to przekroczenie pułapu wynagrodzenia przez zespół. Możliwym sposobem dodania tego byłoby coś takiego:Ach ... ale nie możesz,
override Add
bo to nie jestvirtual
(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: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.
źródło
Czy pozwala ludziom mówić
ma jakiś sens? Jeśli nie, to nie powinna to być lista.
źródło
myTeam.subTeam(3, 5);
subList
już nawet nie powróciTeam
.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.
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.
Dokładnie :) powiesz footballTeam.Add (John), a nie footballTeam.List.Add (John). Wewnętrzna lista nie będzie widoczna.
źródło
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ć
RemovePlayer
metodę, która przyjmuje odpowiedni wyliczenie wejściowe. Jednak dziedzicząc poList
, nie można uniemożliwić bezpośredniego dostępuRemove
,RemoveAll
a nawetClear
. W rezultacie faktycznie osłabiłeś swojąFootballTeam
klasę.Dodatkowe przemyślenia na temat enkapsulacji ... Podniosłeś następujące obawy:
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 Players
na wszystko i różne, aby mogli bawić się z twoim zespołem bez twojej zgody.Dalej mówisz:
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 dzwoniliateam.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.
źródło
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ą.
źródło
List
naraziłoby klientów tej klasy na metody, które mogą nie być narażone ”, właśnie to powoduje, że dziedziczenie poList
absolutnym 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 metodList
, więc nie ujawniaj ich.List
ponieważ 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”.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ą.
źródło
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:
Kiedy znajdę to lepiej niż
Co więcej, moja PlayerCollection może być używana przez inne klasy, takie jak „Club” bez powielania kodu.
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?
lub
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.
źródło
Collection<T>
to nie to samo, coList<T>
może wymagać sporo pracy, aby zweryfikować w dużym projekcie.team.ByName("Nicolas")
oznacza"Nicolas"
nazwę zespołu.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.
źródło
class FootballTeam : List<FootballPlayers>
, użytkownicy mojej klasy będą mogli powiedzieć: odziedziczyłemList<T>
po zastosowaniu refleksji, dostrzeganiuList<T>
metod, które nie mają sensu dla drużyny piłkarskiej, i możliwości korzystaniaFootballTeam
z nichList<T>
, więc ujawniłem klientowi szczegóły implementacji (niepotrzebnie).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 ujawnieniuList<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ą dziedziczeniaList<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.
źródło
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ć swojejTeam
klasie na obsługę rozszerzeń zapytań Linq, bez publicznego ujawniania wszystkich metod i właściwościList<T>
.źródło
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.
źródło
Chciałem tylko dodać, że Bertrand Meyer, wynalazca Eiffla i projektu na podstawie umowy,
Team
odziedziczyłby poList<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
Window
odziedziczył oba te elementyRectangle
iTree<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
Add
iRemove
doHire
iFire
w swojejTeam
. Jeśli instancjaTeam
jest uskok z powrotemList<Player>
, rozmówca będzie korzystałAdd
iRemove
do niego, ale swoje wirtualne metody modyfikacjiHire
iFire
będzie nazwany.źródło
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?
A następnie FootballTeam może wyglądać następująco:
źródło
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ę :
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ócie
System.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.List
typ danych nazywa się „listą”, jest nieco czerwony. ASystem.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:
IEnumerable<T>
IReadonlyList<T>
.ICollection<T>
ISet<T>
?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
IArray
interfejsie dla biblioteki LinqArray .źródło
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.
Dalsza lektura: Gdy klasa jest dziedziczona z List <>, XmlSerializer nie serializuje innych atrybutów
źródło
Cytując Erica Lipperta:
Na przykład jesteś zmęczony brakiem
AddRange
metody wIList<T>
:źródło