Dlaczego wolisz kompozycję niż dziedziczenie? Jakie są kompromisy dla każdego podejścia? Kiedy wybrać dziedziczenie zamiast kompozycji?
language-agnostic
oop
inheritance
composition
aggregation
Tylko czytać
źródło
źródło
Odpowiedzi:
Preferuj kompozycję zamiast dziedziczenia, ponieważ jest ona bardziej plastyczna / łatwa do późniejszego zmodyfikowania, ale nie używaj metody „zawsze komponuj”. Dzięki kompozycji łatwo jest zmienić zachowanie w locie dzięki Dependency Injection / Setters. Dziedziczenie jest bardziej sztywne, ponieważ większość języków nie pozwala wywodzić się z więcej niż jednego typu. Więc gęś jest mniej więcej ugotowana, kiedy zaczniesz czerpać z TypeA.
Mój test kwasu na powyższe to:
Czy TypeB chce udostępnić pełny interfejs (wszystkie metody publiczne nie mniej) TypeA w taki sposób, że TypeB może być używany tam, gdzie oczekuje się TypeA? Wskazuje dziedziczenie .
Czy TypeB chce tylko część / część zachowania ujawnionego przez TypeA? Wskazuje na potrzebę składu.
Aktualizacja: Właśnie wróciłem do mojej odpowiedzi i wydaje się teraz, że jest niekompletna bez konkretnej wzmianki o zasadzie zastąpienia Liskowa przez Barbarę Liskov jako testu na „Czy powinienem dziedziczyć po tym rodzaju?”
źródło
Pomyśl o zamknięciu jako o związku. Samochód „ma” silnik, osoba ”ma„ imię itp.
Pomyśl dziedziczenia jako jest związek. Samochód ”to„ pojazd, osoba ”to„ ssak itp.
Nie podoba mi się to podejście. Wziąłem go prosto z drugiej edycji Code Complete autorstwa Steve'a McConnella , sekcja 6.3 .
źródło
Jeśli zrozumiesz różnicę, łatwiej to wytłumaczyć.
Kod proceduralny
Przykładem tego jest PHP bez użycia klas (szczególnie przed PHP5). Cała logika jest zakodowana w zestawie funkcji. Możesz dołączyć inne pliki zawierające funkcje pomocnicze itp. I prowadzić logikę biznesową, przekazując dane w funkcjach. Może to być bardzo trudne do opanowania w miarę rozwoju aplikacji. PHP5 próbuje temu zaradzić, oferując bardziej obiektowe projektowanie.
Dziedzictwo
To zachęca do korzystania z klas. Dziedziczenie jest jedną z trzech zasad projektowania OO (dziedziczenie, polimorfizm, enkapsulacja).
To jest dziedziczenie w pracy. Pracownik ”to„ Osoba lub dziedziczy od Osoby. Wszystkie relacje dziedziczenia są relacjami typu „jest-a”. Pracownik ukrywa również właściwość Tytuł od Osoby, co oznacza, że Pracownik. Tytuł zwróci Tytuł pracownikowi, a nie osobie.
Kompozycja
Skład preferowany jest nad dziedziczeniem. Mówiąc bardzo prosto, masz:
Kompozycja zazwyczaj ma „relację” lub „używa relacji”. Tutaj klasa pracownika ma osobę. Nie dziedziczy po Person, ale zamiast tego pobiera do niego obiekt Person, dlatego „ma” Person.
Skład nad dziedziczeniem
Teraz powiedz, że chcesz utworzyć typ menedżera, aby uzyskać:
Ten przykład będzie działał dobrze, ale co jeśli Osoba i Pracownik zadeklarują
Title
? Czy Manager.Title powinien zwrócić „Manager of Operations” czy „Mr.”? W przypadku kompozycji ta dwuznaczność jest lepiej obsługiwana:Obiekt menedżera składa się z pracownika i osoby. Zachowanie tytułu pochodzi od pracownika. Ta wyraźna kompozycja usuwa między innymi niejednoznaczność i napotkasz mniej błędów.
źródło
Ze wszystkimi niezaprzeczalnymi korzyściami płynącymi z dziedziczenia, oto niektóre z jego wad.
Wady dziedziczenia:
Z drugiej strony skład obiektów jest definiowany w czasie wykonywania przez obiekty, które uzyskują odniesienia do innych obiektów. W takim przypadku obiekty te nigdy nie będą mogły nawzajem dotrzeć do chronionych danych (bez przerwy w enkapsulacji) i będą zmuszone do wzajemnego respektowania interfejsu. Również w tym przypadku zależności implementacyjne będą znacznie mniejsze niż w przypadku dziedziczenia.
źródło
Kolejny, bardzo pragmatyczny powód, aby preferować kompozycję zamiast dziedziczenia, dotyczy modelu domeny i mapowania go na relacyjną bazę danych. Naprawdę trudno jest odwzorować dziedziczenie na model SQL (w efekcie powstają przeróżne obejścia, takie jak tworzenie kolumn, które nie zawsze są używane, używanie widoków itp.). Niektóre ORML-y próbują sobie z tym poradzić, ale zawsze komplikuje się to szybko. Kompozycję można łatwo modelować za pomocą relacji klucza obcego między dwiema tabelami, ale dziedziczenie jest znacznie trudniejsze.
źródło
Krótko mówiąc, zgadzam się z „Wolę kompozycję niż dziedziczenie”, ale dla mnie to często brzmi „wolę ziemniaki od coca-coli”. Są miejsca na dziedzictwo i miejsca na kompozycję. Musisz zrozumieć różnicę, to pytanie zniknie. To, co naprawdę dla mnie znaczy, to „jeśli zamierzasz korzystać z dziedziczenia - pomyśl jeszcze raz, istnieje szansa, że potrzebujesz kompozycji”.
Wolisz ziemniaki od coca coli, gdy chcesz jeść, a coca cola od ziemniaków, kiedy chcesz się napić.
Utworzenie podklasy powinno oznaczać coś więcej niż tylko wygodny sposób wywoływania metod nadklasy. Powinieneś użyć dziedziczenia, gdy podklasa „jest” super klasą zarówno strukturalnie, jak i funkcjonalnie, kiedy może być użyta jako nadklasa i zamierzasz jej użyć. Jeśli tak nie jest - nie jest to dziedzictwo, ale coś innego. Kompozycja ma miejsce, gdy twoje obiekty składają się z innego lub mają z nimi jakiś związek.
Wygląda więc na to, że jeśli ktoś nie wie, czy potrzebuje dziedzictwa lub składu, prawdziwym problemem jest to, że nie wie, czy chce pić, czy jeść. Pomyśl o swojej problematycznej domenie, lepiej ją zrozum.
źródło
InternalCombustionEngine
z klasą pochodnąGasolineEngine
. Ten ostatni dodaje rzeczy takie jak świece zapłonowe, których brakuje w klasie podstawowej, ale użycie tego jakoInternalCombustionEngine
spowoduje, że świece zapłonowe będą używane.Dziedzictwo jest dość kuszące, zwłaszcza z ziemi proceduralnej i często wygląda na pozornie eleganckie. Mam na myśli, że wszystko, co muszę zrobić, to dodać tę odrobinę funkcjonalności do innej klasy, prawda? Jednym z problemów jest to
Dziedziczenie jest prawdopodobnie najgorszą formą sprzężenia, jaką możesz mieć
Twoja klasa podstawowa przerywa enkapsulację, ujawniając szczegóły implementacji podklasom w postaci chronionych elementów. Dzięki temu Twój system jest sztywny i delikatny. Jednak najbardziej tragiczną wadą jest to, że nowa podklasa niesie ze sobą cały bagaż i opinię łańcucha spadkowego.
W artykule Inheritance is Evil: The Epic Fail of DataAnnotationsModelBinder przedstawiono przykład takiego rozwiązania w języku C #. Pokazuje wykorzystanie dziedziczenia, kiedy kompozycja powinna była zostać użyta, i jak można ją zrefaktoryzować.
źródło
W Javie lub C # obiekt nie może zmienić swojego typu po utworzeniu instancji.
Tak więc, jeśli twój obiekt musi wyglądać jak inny obiekt lub zachowywać się inaczej w zależności od stanu obiektu lub warunków, użyj kompozycji : zapoznaj się z wzorcami stanu i strategii .
Jeśli obiekt musi być tego samego typu, użyj Dziedziczenia lub zaimplementuj interfejsy.
źródło
Client
. NastępniePreferredClient
pojawia się nowa koncepcja wyskakującego okienka. Czy powinienPreferredClient
odziedziczyćClient
? Preferowanym klientem jest mimo wszystko klient, nie? Cóż, nie tak szybko ... jak powiedziałeś, obiekty nie mogą zmienić swojej klasy w czasie wykonywania. Jak byś modelowałclient.makePreferred()
operację? Być może odpowiedź polega na użyciu kompozycji z brakującą koncepcjąAccount
?Client
klas, być może istnieje tylko jedna, która zawiera koncepcjęAccount
StandardAccount
PreferredAccount
Nie znalazłem tutaj satysfakcjonującej odpowiedzi, więc napisałem nową.
Aby zrozumieć, dlaczego „ wolę kompozycję niż dziedziczenie”, musimy najpierw odzyskać założenie pominięte w tym skróconym języku.
Istnieją dwie zalety dziedziczenia: podtypowanie i podklasowanie
Podpisywanie oznacza zgodność z podpisem typu (interfejsu), tj. Zestawem interfejsów API, i można zastąpić część podpisu, aby uzyskać polimorfizm podtypu.
Podklasowanie oznacza niejawne ponowne wykorzystanie implementacji metod.
Z tymi dwiema korzyściami wiążą się dwa różne cele dziedziczenia: zorientowane na podtypy i zorientowane na ponowne użycie kodu.
Jeśli ponowne użycie kodu jest jedynym celem, podklasowanie może dać o wiele więcej niż potrzebuje, tzn. Niektóre publiczne metody klasy nadrzędnej nie mają większego sensu dla klasy podrzędnej. W takim przypadku zamiast preferować kompozycję zamiast dziedziczenia, wymagana jest kompozycja . Stąd też pochodzi pojęcie „to-a” kontra „ma-a”.
Tak więc tylko wtedy, gdy zamierzone jest podtypowanie, tj. Użycie nowej klasy później w sposób polimorficzny, stajemy przed problemem wyboru dziedziczenia lub kompozycji. Jest to założenie pomijane w omawianym skróconym języku.
Podtyp jest zgodny z podpisem typu, co oznacza, że kompozycja zawsze musi ujawniać nie mniejszą liczbę interfejsów API tego typu. Teraz zaczynają się kompromisy:
Dziedziczenie zapewnia proste ponowne użycie kodu, jeśli nie zostanie zastąpione, natomiast kompozycja musi ponownie kodować każdy interfejs API, nawet jeśli jest to zwykłe zadanie przekazania.
Dziedziczenie zapewnia bezpośrednią otwartą rekurencję za pośrednictwem wewnętrznej strony polimorficznej
this
, tj. Wywoływanie metody zastępowania (lub nawet typu ) w innej funkcji składowej, publicznej lub prywatnej (choć odradzane ). Otwarta rekurencja może być symulowana przez kompozycję , ale wymaga dodatkowego wysiłku i może nie zawsze być wykonalna (?). Ta odpowiedź na zduplikowane pytanie mówi coś podobnego.Dziedziczenie ujawnia członków chronionych . To przerywa enkapsulację klasy nadrzędnej, a jeśli jest używana przez podklasę, wprowadza się inną zależność między dzieckiem a jego rodzicem.
Kompozycja ma funkcję odwracania kontroli, a jej zależność można wstrzykiwać dynamicznie, jak pokazano we wzorze dekoratora i wzorze zastępczym .
Kompozycja ma zaletę programowania zorientowanego na kombinator , tzn. Działa w sposób podobny do wzorca złożonego .
Kompozycja natychmiast następuje po zaprogramowaniu interfejsu .
Kompozycja ma zaletę łatwego wielokrotnego dziedziczenia .
Mając na uwadze powyższe kompromisy, wolimy więc kompozycję niż dziedziczenie. Jednak w przypadku ściśle ze sobą powiązanych klas, tj. Gdy niejawne ponowne użycie kodu naprawdę przynosi korzyści lub pożądana jest magiczna moc otwartej rekurencji, wybór należy do dziedziczenia.
źródło
Osobiście nauczyłem się, że zawsze wolę kompozycję niż dziedziczenie. Nie ma problemu programowego, który można rozwiązać za pomocą dziedziczenia, którego nie można rozwiązać za pomocą kompozycji; chociaż w niektórych przypadkach może być konieczne użycie interfejsów (Java) lub protokołów (Obj-C). Ponieważ C ++ nic nie wie, będziesz musiał używać abstrakcyjnych klas bazowych, co oznacza, że nie możesz całkowicie pozbyć się dziedziczenia w C ++.
Kompozycja jest często bardziej logiczna, zapewnia lepszą abstrakcję, lepszą enkapsulację, lepsze ponowne użycie kodu (szczególnie w bardzo dużych projektach) i jest mniej prawdopodobne, że cokolwiek zepsuje na odległość tylko dlatego, że dokonałeś izolowanej zmiany w dowolnym miejscu kodu. Ułatwia także przestrzeganie „ zasady pojedynczej odpowiedzialności ”, która często jest streszczona jako „ Nigdy nie powinno być więcej niż jednego powodu, aby klasa się zmieniła ”, a to oznacza, że każda klasa istnieje dla określonego celu i powinna mają tylko metody bezpośrednio związane z jego przeznaczeniem. Posiadanie również bardzo płytkiego drzewa dziedziczenia znacznie ułatwia prowadzenie przeglądu, nawet gdy projekt staje się naprawdę duży. Wiele osób uważa, że dziedzictwo reprezentuje nasze prawdziwy światcałkiem dobrze, ale to nie jest prawda. Świat rzeczywisty wykorzystuje znacznie więcej kompozycji niż dziedziczenia. Niemal każdy obiekt z prawdziwego świata, który możesz trzymać w dłoni, został złożony z innych, mniejszych obiektów z prawdziwego świata.
Są jednak wady kompozycji. Jeśli całkowicie pominiesz dziedziczenie i skupisz się tylko na kompozycji, zauważysz, że często musisz napisać kilka dodatkowych linii kodu, które nie byłyby konieczne, jeśli użyłeś dziedziczenia. Czasami jesteś też zmuszony do powtarzania się, co narusza ZASADĘ SUCHANIA(DRY = Don't Repeat Yourself). Również kompozycja często wymaga delegowania, a metoda wywołuje inną metodę innego obiektu bez żadnego innego kodu otaczającego to wywołanie. Takie „podwójne wywołania metod” (które mogą z łatwością rozszerzyć się na potrójne lub poczwórne wywołania metod, a nawet dalej) mają znacznie gorszą wydajność niż dziedziczenie, w którym po prostu dziedziczy się metodę rodzica. Wywołanie metody odziedziczonej może być równie szybkie jak wywołanie metody nie odziedziczonej lub może być nieco wolniejsze, ale zwykle jest jeszcze szybsze niż dwa kolejne wywołania metody.
Być może zauważyłeś, że większość języków OO nie zezwala na wielokrotne dziedziczenie. Chociaż istnieje kilka przypadków, w których wielokrotne dziedziczenie może naprawdę coś kupić, ale są to raczej wyjątki niż reguła. Ilekroć natkniesz się na sytuację, w której myślisz, że „wielokrotne dziedziczenie byłoby naprawdę fajną funkcją rozwiązania tego problemu”, zwykle znajdujesz się w punkcie, w którym powinieneś ponownie przemyśleć dziedziczenie, ponieważ nawet może to wymagać kilku dodatkowych linii kodu , rozwiązanie oparte na składzie zwykle okaże się znacznie bardziej eleganckie, elastyczne i odporne na przyszłość.
Dziedziczenie to naprawdę fajna funkcja, ale obawiam się, że była nadużywana przez ostatnie kilka lat. Ludzie traktowali dziedziczenie jako jeden młotek, który może to wszystko przybić, bez względu na to, czy rzeczywiście był to gwóźdź, śruba, czy może coś zupełnie innego.
źródło
TextFile
is aFile
.Moja ogólna zasada: przed użyciem dziedziczenia zastanów się, czy kompozycja ma większy sens.
Powód: Podklasowanie zwykle oznacza większą złożoność i powiązania, tj. Trudniejsze do zmiany, utrzymania i skalowania bez popełniania błędów.
O wiele bardziej kompletna i konkretna odpowiedź Tima Boudreau z Słońca:
źródło
Dlaczego wolisz kompozycję niż dziedziczenie?
Zobacz inne odpowiedzi.
Kiedy możesz skorzystać z dziedziczenia?
Często mówi się, że klasa
Bar
może odziedziczyć klasę,Foo
gdy prawdziwe jest następujące zdanie:Niestety sam powyższy test nie jest wiarygodny. Zamiast tego użyj następujących opcji:
Pierwsze testy, zapewnia, że wszystkie getters o
Foo
sens wBar
(= wspólne właściwości), natomiast drugie badanie daje pewność, że wszystkie setery oFoo
sens wBar
(= wspólne funkcjonalność).Przykład 1: Pies -> Zwierzę
Pies to zwierzę ORAZ psy mogą robić wszystko, co mogą robić zwierzęta (takie jak oddychanie, umieranie itp.). Dlatego klasa
Dog
może odziedziczyć klasęAnimal
.Przykład 2: Okrąg - / -> Elipsa
Okrąg jest elipsą, ALE koła nie mogą zrobić wszystkiego, co elipsy mogą zrobić. Na przykład koła nie mogą się rozciągać, a elipsy mogą. Dlatego klasa
Circle
nie może dziedziczyć klasyEllipse
.Nazywa się to problemem Koła-Elipsy , co tak naprawdę nie jest problemem, jest tylko wyraźnym dowodem, że sam pierwszy test nie wystarczy, aby stwierdzić, że dziedziczenie jest możliwe. W szczególności w tym przykładzie podkreślono, że klasy pochodne powinny rozszerzać funkcjonalność klas podstawowych, nigdy go nie ograniczać . W przeciwnym razie klasa podstawowa nie mogłaby zostać użyta polimorficznie.
Kiedy należy zastosować dziedziczenie?
Nawet jeśli można użyć dziedziczenia nie znaczy, że należy : za pomocą składu jest zawsze opcja. Dziedziczenie to potężne narzędzie umożliwiające niejawne ponowne użycie kodu i dynamiczne wysyłanie, ale ma kilka wad, dlatego często preferowana jest kompozycja. Kompromisy między dziedziczeniem a kompozycją nie są oczywiste i moim zdaniem najlepiej wyjaśnić je w odpowiedzi lcn .
Jako ogólną zasadę wybieram dziedziczenie zamiast kompozycji, gdy oczekuje się, że użycie polimorficzne będzie bardzo powszechne, w którym to przypadku siła dynamicznej wysyłki może prowadzić do znacznie bardziej czytelnego i eleganckiego API. Na przykład posiadanie klasy polimorficznej
Widget
w ramach GUI lub klasy polimorficznejNode
w bibliotekach XML pozwala mieć interfejs API, który jest o wiele bardziej czytelny i intuicyjny w użyciu niż w przypadku rozwiązania opartego wyłącznie na składzie.Zasada Liskowa
Dla pewności inna metoda stosowana do ustalenia, czy możliwe jest dziedziczenie, nazywa się zasadą podstawienia Liskowa :
Zasadniczo oznacza to, że dziedziczenie jest możliwe, jeśli klasę podstawową można zastosować polimorficznie, co moim zdaniem jest równoważne z naszym testem „słupek to foo, a słupki mogą zrobić wszystko, co potrafią foos”.
źródło
computeArea(Circle* c) { return pi * square(c->radius()); }
. Jest oczywiście zepsuty, jeśli przeszedł przez elipsę (co to znaczy nawet promień ()?). Elipsa nie jest kołem i jako taka nie powinna pochodzić od koła.computeArea(Circle *c) { return pi * width * height / 4.0; }
Teraz jest ogólny.width()
iheight()
? Co jeśli użytkownik biblioteki zdecyduje się utworzyć kolejną klasę o nazwie „EggShape”? Czy powinien pochodzić również z „Koła”? Oczywiście nie. Jajeczny kształt nie jest kołem, a elipsa też nie jest kołem, więc żadne nie powinno pochodzić z Koła, ponieważ łamie LSP. Metody wykonujące operacje na klasie Circle * przyjmują silne założenia co do tego, czym jest koło, a ich złamanie prawie na pewno doprowadzi do błędów.Dziedziczenie jest bardzo potężne, ale nie można go wymusić (patrz: problem elipsy koła ). Jeśli naprawdę nie możesz być całkowicie pewien prawdziwej relacji typu „to-a”, najlepiej wybrać kompozycję.
źródło
Dziedziczenie tworzy silny związek między podklasą i superklasą; podklasa musi znać szczegóły implementacji superklasy. Tworzenie superklasy jest znacznie trudniejsze, gdy trzeba pomyśleć o tym, jak można ją przedłużyć. Musisz dokładnie udokumentować niezmienniki klas i określić, jakie inne metody nadpisują metody używane wewnętrznie.
Dziedziczenie jest czasem przydatne, jeśli hierarchia naprawdę reprezentuje relację typu „a-a-relacja”. Odnosi się do zasady Open-Closed Principle, która stwierdza, że klasy powinny być zamknięte w celu modyfikacji, ale otwarte na rozszerzenie. W ten sposób możesz mieć polimorfizm; mieć ogólną metodę, która zajmuje się supertypem i jego metodami, ale poprzez dynamiczną dyspozycję wywoływana jest metoda podklasy. Jest to elastyczne i pomaga stworzyć pośrednictwo, które jest niezbędne w oprogramowaniu (mniej wiedzieć o szczegółach implementacji).
Dziedziczenie jest jednak łatwo nadużywane i powoduje dodatkową złożoność, z twardymi zależnościami między klasami. Również zrozumienie tego, co dzieje się podczas wykonywania programu, staje się dość trudne ze względu na warstwy i dynamiczny wybór wywołań metod.
Sugerowałbym użycie komponowania jako domyślnego. Jest bardziej modułowy i daje korzyść z późnego wiązania (można dynamicznie zmieniać komponent). Łatwiej jest też przetestować rzeczy osobno. A jeśli musisz użyć metody z klasy, nie musisz być w określonej formie (Zasada Zastępowania Liskowa).
źródło
Inheritance is sometimes useful... That way you can have polymorphism
jako twarde powiązanie pojęć dziedziczenia i polimorfizmu (zakładanie podtypów w kontekście). Mój komentarz miał na celu wskazanie tego, co wyjaśnisz w swoim komentarzu: że dziedziczenie nie jest jedynym sposobem na wdrożenie polimorfizmu i w rzeczywistości niekoniecznie jest decydującym czynnikiem przy podejmowaniu decyzji między składem a spadkiem.Załóżmy, że samolot ma tylko dwie części: silnik i skrzydła.
Istnieją dwa sposoby zaprojektowania klasy samolotu.
Teraz twój samolot może zacząć od ustawiania stałych skrzydeł
i zamiany ich na skrzydła obrotowe w locie. Zasadniczo jest
to silnik ze skrzydłami. Ale co jeśli chciałbym również zmienić
silnik w locie?
Albo klasa podstawowa
Engine
naraża mutatora na zmianę jegowłaściwości, albo przeprojektowuję
Aircraft
jako:Teraz mogę również wymienić silnik na bieżąco.
źródło
Musisz rzucić okiem na zasadę substytucji Liskowa w SOLIDNYCH zasadach projektowania klas wuja Boba . :)
źródło
Jeśli chcesz „skopiować” / odsłonić interfejs API klasy podstawowej, użyj dziedziczenia. Jeśli chcesz tylko skopiować funkcję, skorzystaj z delegowania.
Jeden przykład tego: chcesz utworzyć stos z listy. Stack ma tylko pop, push i peek. Nie powinieneś używać dziedziczenia, biorąc pod uwagę, że nie chcesz push_back, push_front, removeAt i innych podobnych funkcji w stosie.
źródło
Te dwa sposoby mogą dobrze żyć razem i faktycznie wspierać się nawzajem.
Składanie jest po prostu modularne: tworzysz interfejs podobny do klasy nadrzędnej, tworzysz nowy obiekt i przekazujesz do niego wywołania. Jeśli te obiekty nie muszą się znać, jest to dość bezpieczna i łatwa w użyciu kompozycja. Jest tu tak wiele możliwości.
Jeśli jednak klasa nadrzędna z jakiegoś powodu potrzebuje dostępu do funkcji dostarczonych przez „klasę podrzędną” dla niedoświadczonego programisty, może to wyglądać, że jest to doskonałe miejsce do dziedziczenia. Klasa nadrzędna może po prostu nazwać swoją własną abstrakcję „foo ()”, która jest zastępowana przez podklasę, a następnie może nadać wartość abstrakcyjnej bazie.
Wygląda na fajny pomysł, ale w wielu przypadkach lepiej po prostu dać klasie obiekt, który implementuje foo () (lub nawet ustawić wartość podaną foo () ręcznie), niż odziedziczyć nową klasę z jakiejś klasy podstawowej, która wymaga należy określić funkcję foo ().
Dlaczego?
Ponieważ dziedziczenie jest złym sposobem przenoszenia informacji .
Kompozycja ma tutaj prawdziwą przewagę: relację można odwrócić: „klasa nadrzędna” lub „pracownik abstrakcyjny” może agregować dowolne konkretne obiekty „podrzędne” implementujące określony interfejs + dowolne dziecko może zostać ustawione w dowolnym innym typie rodzica, który akceptuje to jest typ . I może istnieć dowolna liczba obiektów, na przykład MergeSort lub QuickSort może sortować dowolną listę obiektów implementujących abstrakcyjny interfejs porównania. Innymi słowy: każda grupa obiektów, które implementują „foo ()” i inna grupa obiektów, które mogą korzystać z obiektów posiadających „foo ()”, mogą grać razem.
Mogę wymyślić trzy prawdziwe powody używania dziedziczenia:
Jeśli są one prawdziwe, prawdopodobnie konieczne jest zastosowanie dziedziczenia.
Nie ma nic złego w korzystaniu z przyczyny 1, bardzo dobrze jest mieć solidny interfejs na swoich obiektach. Można to zrobić przy użyciu kompozycji lub dziedziczenia, nie ma problemu - jeśli ten interfejs jest prosty i nie zmienia się. Zwykle dziedziczenie jest tutaj dość skuteczne.
Jeśli powodem jest numer 2, staje się to nieco trudne. Czy naprawdę potrzebujesz tylko tej samej klasy podstawowej? Ogólnie rzecz biorąc, samo użycie tej samej klasy bazowej nie jest wystarczająco dobre, ale może być wymogiem twojego frameworka, rozważania projektowego, którego nie można uniknąć.
Jeśli jednak chcesz użyć zmiennych prywatnych, przypadek 3, możesz mieć kłopoty. Jeśli uważasz, że zmienne globalne są niebezpieczne, powinieneś rozważyć zastosowanie dziedziczenia, aby uzyskać dostęp do zmiennych prywatnych również niebezpiecznych . Pamiętaj, że zmienne globalne nie są wcale takie złe - bazy danych są zasadniczo dużym zestawem zmiennych globalnych. Ale jeśli sobie z tym poradzisz, to jest całkiem w porządku.
źródło
Aby odpowiedzieć na to pytanie z innej perspektywy dla nowszych programistów:
Dziedzictwo jest często nauczane wcześnie, kiedy uczymy się programowania obiektowego, dlatego jest postrzegane jako łatwe rozwiązanie typowego problemu.
Brzmi świetnie, ale w praktyce prawie nigdy, nigdy nie działa, z jednego z kilku powodów:
W końcu łączymy nasz kod w kilka trudnych węzłów i nie czerpiemy z niego żadnych korzyści poza tym, że możemy powiedzieć: „Fajnie, nauczyłem się o dziedziczeniu, a teraz go użyłem”. To nie powinno być protekcjonalne, ponieważ wszyscy to zrobiliśmy. Ale wszyscy to zrobiliśmy, ponieważ nikt nam nie powiedział.
Gdy tylko ktoś wyjaśnił mi, że „faworyzuj kompozycję zamiast dziedziczenia”, zastanawiałem się za każdym razem, gdy próbowałem dzielić funkcjonalność między klasami za pomocą dziedziczenia i zdałem sobie sprawę, że przez większość czasu tak naprawdę nie działało to dobrze.
Antidotum to zasada pojedynczej odpowiedzialności . Pomyśl o tym jako o ograniczeniu. Moja klasa musi zrobić jedną rzecz. I musi być w stanie dać mojej klasie nazwę, która w jakiś sposób opisuje, że jedna rzecz to robi. (Istnieją wyjątki od wszystkiego, ale reguły bezwzględne są czasem lepsze, gdy się uczymy.) Wynika z tego, że nie mogę napisać klasy bazowej o nazwie
ObjectBaseThatContainsVariousFunctionsNeededByDifferentClasses
. Jakakolwiek odrębna funkcjonalność, której potrzebuję, musi należeć do własnej klasy, a następnie inne klasy, które potrzebują tej funkcjonalności, mogą zależeć od tej klasy, a nie dziedziczyć po niej.Ryzyko nadmiernego uproszczenia polega na złożeniu - złożeniu wielu klas do wspólnej pracy. A kiedy ukształtujemy ten nawyk, stwierdzimy, że jest on znacznie bardziej elastyczny, łatwy w utrzymaniu i testowalny niż w przypadku dziedziczenia.
źródło
Oprócz rozważań, należy także wziąć pod uwagę „głębokość” dziedziczenia, przez którą musi przejść Twój obiekt. Wszystko powyżej pięciu lub sześciu poziomów dziedziczenia głębokiego może powodować nieoczekiwane problemy z rzutowaniem i boksowaniem / rozpakowywaniem, a w takich przypadkach rozsądne może być skomponowanie obiektu.
źródło
Kiedy masz to-a relacja pomiędzy dwiema klasami (przykład pies jest psi), idziesz do dziedziczenia.
Z drugiej strony, kiedy masz ma-a lub jakiś związek między dwiema klasami przymiotnik (student ma kursów) lub (studia nauczycielskie kursy), wybrał skład.
źródło
Prostym sposobem na zrozumienie tego byłoby użycie dziedziczenia, gdy potrzebujesz obiektu swojej klasy, aby miał ten sam interfejs co jego klasa nadrzędna, aby można go w ten sposób traktować jako obiekt klasy nadrzędnej (upcasting) . Co więcej, wywołania funkcji na obiekcie klasy pochodnej pozostałyby takie same w całym kodzie, ale określona metoda wywołania byłaby określana w czasie wykonywania (tj. Implementacja niskiego poziomu różni się, interfejs wysokiego poziomu pozostaje taki sam).
Kompozycji należy używać, gdy nowa klasa nie ma tego samego interfejsu, tzn. Chcesz ukryć pewne aspekty implementacji klasy, o których użytkownik tej klasy nie musi wiedzieć. Więc kompozycja jest bardziej na drodze do wsparcia enkapsulacji (tj. Ukrywaniu implementacji), podczas gdy dziedziczenie ma wspierać abstrakcję (tj. Zapewnia uproszczoną reprezentację czegoś, w tym przypadku ten sam interfejs dla szeregu typów z różnymi elementami wewnętrznymi).
źródło
Podpisywanie jest właściwe i bardziej wydajne, gdzie niezmienniki można wyliczyć , w przeciwnym razie należy użyć kompozycji funkcji dla rozszerzenia.
źródło
Zgadzam się z @Pavel, kiedy mówi, że są miejsca na kompozycję i są miejsca na dziedzictwo.
Uważam, że należy zastosować dziedziczenie, jeśli twoja odpowiedź jest twierdząca na którekolwiek z tych pytań.
Jeśli jednak intencją jest wyłącznie ponowne użycie kodu, wówczas najprawdopodobniej kompozycja jest lepszym wyborem projektowym.
źródło
Dziedziczenie to bardzo potężny mechanizm ponownego użycia kodu. Ale musi być właściwie używane. Powiedziałbym, że dziedziczenie jest używane poprawnie, jeśli podklasa jest również podtypem klasy nadrzędnej. Jak wspomniano powyżej, podstawową kwestią jest tutaj zasada zastąpienia Liskowa.
Podklasa to nie to samo co podtyp. Możesz tworzyć podklasy, które nie są podtypami (i właśnie wtedy powinieneś użyć kompozycji). Aby zrozumieć, co to jest podtyp, zacznijmy od wyjaśnienia, czym jest typ.
Gdy mówimy, że liczba 5 jest liczbą całkowitą, stwierdzamy, że 5 należy do zestawu możliwych wartości (na przykład zobacz możliwe wartości dla pierwotnych typów Java). Stwierdzamy również, że istnieje prawidłowy zestaw metod, które mogę wykonać na wartościach takich jak dodawanie i odejmowanie. I wreszcie stwierdzamy, że istnieje zestaw właściwości, które są zawsze spełnione, na przykład, jeśli dodam wartości 3 i 5, w rezultacie otrzymam 8.
Aby podać inny przykład, pomyśl o abstrakcyjnych typach danych, zestawie liczb całkowitych i liście liczb całkowitych, wartości, które mogą przechowywać, są ograniczone do liczb całkowitych. Oba obsługują zestaw metod, takich jak add (newValue) i size (). I oba mają różne właściwości (niezmiennik klasy), Zestawy nie zezwalają na duplikaty, podczas gdy Lista zezwala na duplikaty (oczywiście istnieją inne właściwości, które oba spełniają).
Podtyp jest także typem, który ma związek z innym typem, zwanym typem nadrzędnym (lub nadtypem). Podtyp musi spełniać funkcje (wartości, metody i właściwości) typu nadrzędnego. Relacja oznacza, że w każdym kontekście, w którym oczekiwany jest nadtyp, można go zastąpić podtypem, bez wpływu na zachowanie wykonania. Chodźmy zobaczyć kod, aby zilustrować to, co mówię. Załóżmy, że piszę listę liczb całkowitych (w jakimś pseudo języku):
Następnie piszę Zbiór liczb całkowitych jako podklasę Listy liczb całkowitych:
Nasz zestaw liczb całkowitych jest podklasą Listy liczb całkowitych, ale nie jest podtypem, ponieważ nie spełnia wszystkich funkcji klasy List. Wartości i podpis metod są spełnione, ale właściwości nie. Zachowanie metody add (Integer) zostało wyraźnie zmienione, nie zachowując właściwości typu nadrzędnego. Myśl z punktu widzenia klienta twoich zajęć. Mogą otrzymać zestaw liczb całkowitych, na których spodziewana jest lista liczb całkowitych. Klient może chcieć dodać wartość i uzyskać tę wartość dodaną do listy, nawet jeśli ta wartość już istnieje na liście. Ale nie dostanie takiego zachowania, jeśli wartość istnieje. Wielka niespodzianka dla niej!
Jest to klasyczny przykład niewłaściwego wykorzystania dziedziczenia. W takim przypadku użyj kompozycji.
(fragment z: poprawnie używaj dziedziczenia ).
źródło
Zasugerowaną przeze mnie zasadą jest, że dziedziczenie powinno być stosowane, gdy jest to relacja „a-a”, a kompozycja, gdy ma „a-a”. Mimo to uważam, że zawsze powinieneś skłaniać się ku kompozycji, ponieważ eliminuje to wiele złożoności.
źródło
Kompozycja v / s Dziedziczenie jest szerokim tematem. Nie ma prawdziwej odpowiedzi na to, co jest lepsze, ponieważ myślę, że wszystko zależy od projektu systemu.
Zasadniczo rodzaj relacji między obiektami zapewnia lepszą informację do wyboru jednego z nich.
Jeśli typem relacji jest relacja „IS-A”, dziedziczenie jest lepszym podejściem. w przeciwnym razie typ relacji to relacja „HAS-A”, wówczas kompozycja lepiej się zbliża.
To całkowicie zależy od relacji między podmiotami.
źródło
Chociaż preferowana jest Kompozycja, chciałbym zwrócić uwagę na zalety Dziedzictwa i wady Kompozycji .
Zalety dziedziczenia:
Ustanawia logiczną relację „ JEST A” . Jeśli samochód i ciężarówka to dwa typy pojazdów (klasa podstawowa), klasa potomna JEST klasą podstawową.
to znaczy
Samochód to pojazd
Ciężarówka to pojazd
Dzięki dziedziczeniu możesz definiować / modyfikować / rozszerzać możliwości
Wady składu:
np. jeśli Samochód zawiera pojazd i jeśli musisz uzyskać cenę samochodu , która została zdefiniowana w pojeździe , Twój kod będzie taki jak ten
źródło
Jak wiele osób powiedziało, zacznę od sprawdzenia, czy istnieje relacja „jest”. Jeśli istnieje, zwykle sprawdzam następujące elementy:
Np. 1. Księgowy jest pracownikiem. Ale ja nie używać dziedziczenia, ponieważ obiekt pracownik może być instancja.
Np. 2. Książka jest przedmiotem sprzedaży. Nie można utworzyć instancji elementu SellingItem - jest to pojęcie abstrakcyjne. Dlatego użyję dziedziczenia. SellingItem to abstrakcyjna klasa bazowa (lub interfejs w języku C #)
Co sądzisz o tym podejściu?
Ponadto popieram odpowiedź @anon w Dlaczego w ogóle korzystać z dziedziczenia?
@MatthieuM. mówi w /software/12439/code-smell-inheritance-abuse/12448#comment303759_12448
ODNIESIENIE
źródło
Nie widzę, żeby nikt wspominał o problemie z diamentem , który mógłby powstać wraz z dziedziczeniem.
Na pierwszy rzut oka, jeśli klasy B i C dziedziczą A i obie przesłaniają metodę X, a czwarta klasa D dziedziczy zarówno od B, jak i C, i nie zastępuje X, jakiej implementacji XD należy użyć?
Wikipedia oferuje ładny przegląd tematu omawianego w tym pytaniu.
źródło