Dlaczego wolisz kompozycję niż dziedziczenie? Jakie są kompromisy dla każdego podejścia? Kiedy wybrać dziedziczenie zamiast kompozycji?
Dlaczego wolisz kompozycję niż dziedziczenie? Jakie są kompromisy dla każdego podejścia? Kiedy wybrać dziedziczenie zamiast kompozycji?
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?”
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 .
Jeśli zrozumiesz różnicę, łatwiej to wytłumaczyć.
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.
To zachęca do korzystania z klas. Dziedziczenie jest jedną z trzech zasad projektowania OO (dziedziczenie, polimorfizm, enkapsulacja).
class Person {
String Title;
String Name;
Int Age
}
class Employee : Person {
Int Salary;
String Title;
}
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.
Skład preferowany jest nad dziedziczeniem. Mówiąc bardzo prosto, masz:
class Person {
String Title;
String Name;
Int Age;
public Person(String title, String name, String age) {
this.Title = title;
this.Name = name;
this.Age = age;
}
}
class Employee {
Int Salary;
private Person person;
public Employee(Person p, Int salary) {
this.person = p;
this.Salary = salary;
}
}
Person johnny = new Person ("Mr.", "John", 25);
Employee john = new Employee (johnny, 50000);
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.
Teraz powiedz, że chcesz utworzyć typ menedżera, aby uzyskać:
class Manager : Person, Employee {
...
}
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:
Class Manager {
public string Title;
public Manager(Person p, Employee e)
{
this.Title = e.Title;
}
}
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.
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.
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.
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.
InternalCombustionEngine
z klasą pochodną GasolineEngine
. Ten ostatni dodaje rzeczy takie jak świece zapłonowe, których brakuje w klasie podstawowej, ale użycie tego jako InternalCombustionEngine
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
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ć.
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.
Client
. Następnie PreferredClient
pojawia się nowa koncepcja wyskakującego okienka. Czy powinien PreferredClient
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.
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.
TextFile
is a File
.
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:
Typowe problemy z używaniem dziedziczenia, tak jak to widzę, to:
- Niewinne działania mogą mieć nieoczekiwane rezultaty - Klasycznym przykładem tego są wywołania metod nadpisywalnych z konstruktora nadklasy, zanim zainicjowane zostaną pola instancji podklas. W idealnym świecie nikt tego nigdy nie zrobiłby. To nie jest idealny świat.
- Stwarza przewrotne pokusy, by podklasatorzy przyjmowali założenia dotyczące kolejności wywołań metod i takie - takie założenia zwykle nie są stabilne, jeśli nadklasa może ewoluować w czasie. Zobacz także moją analogię do tostera i dzbanka do kawy .
- Klasy stają się cięższe - niekoniecznie wiesz, jaką pracę wykonuje twoja nadklasa w swoim konstruktorze, ani ile pamięci zajmie. Tak więc zbudowanie jakiegoś niewinnego, lekkiego obiektu może być znacznie droższe, niż myślisz, i może się to z czasem zmienić, jeśli nadklasa się rozwinie
- Zachęca do eksplozji podklas . Ładowanie klas kosztuje czas, więcej klas kosztuje pamięć. Może to nie być problemem, dopóki nie masz do czynienia z aplikacją w skali NetBeans, ale tam mieliśmy prawdziwe problemy z, na przykład, powolnym menu, ponieważ pierwsze wyświetlenie menu wywołało masowe ładowanie klas. Rozwiązaliśmy ten problem, przechodząc do bardziej deklaratywnej składni i innych technik, ale to również wymagało czasu.
- Utrudnia to późniejsze zmiany - jeśli uczynisz klasę publiczną, zamiana superklasy spowoduje rozbicie podklas - jest to wybór, z którym po upublicznieniu kodu jesteś żonaty. Jeśli więc nie zmieniasz prawdziwej funkcjonalności superklasy, zyskujesz znacznie więcej swobody, aby później zmieniać rzeczy, jeśli używasz, zamiast rozszerzać to, czego potrzebujesz. Weźmy na przykład podklasę JPanel - zwykle jest to źle; a jeśli podklasa jest gdzieś publiczna, nigdy nie masz szansy na ponowne podjęcie tej decyzji. Jeśli jest dostępny jako JComponent getThePanel (), nadal możesz to zrobić (wskazówka: ujawnij modele komponentów jako interfejs API).
- Hierarchie obiektów nie skalują się (lub ich późniejsze skalowanie jest znacznie trudniejsze niż planowanie z wyprzedzeniem) - jest to klasyczny problem „zbyt wielu warstw”. Przejdę do tego poniżej i tego, jak wzór AskTheOracle może go rozwiązać (choć może obrażać purystów OOP).
...
Moje zdanie na temat tego, co należy zrobić, jeśli pozwolisz na dziedziczenie, które możesz wziąć z ziarnem soli, to:
- Nigdy nie ujawniaj żadnych pól oprócz stałych
- Metody muszą być abstrakcyjne lub ostateczne
- Nie wywołuj żadnych metod od konstruktora nadklasy
...
wszystko to dotyczy mniej małych projektów niż dużych, a mniej klas prywatnych niż publicznych
Zobacz inne odpowiedzi.
Często mówi się, że klasa Bar
może odziedziczyć klasę, Foo
gdy prawdziwe jest następujące zdanie:
- bar to głupek
Niestety sam powyższy test nie jest wiarygodny. Zamiast tego użyj następujących opcji:
- bar to głupek ORAZ
- bary mogą zrobić wszystko, co potrafią piłkarzyki.
Pierwsze testy, zapewnia, że wszystkie getters o Foo
sens w Bar
(= wspólne właściwości), natomiast drugie badanie daje pewność, że wszystkie setery o Foo
sens w Bar
(= 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ć klasy Ellipse
.
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.
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 polimorficznej Node
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.
Dla pewności inna metoda stosowana do ustalenia, czy możliwe jest dziedziczenie, nazywa się zasadą podstawienia Liskowa :
Funkcje korzystające ze wskaźników lub referencji do klas podstawowych muszą mieć możliwość korzystania z obiektów klas pochodnych bez ich znajomości
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”.
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()
i height()
? 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ę.
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).
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.
Class Aircraft extends Engine{
var wings;
}
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ę jego
właściwości, albo przeprojektowuję Aircraft
jako:
Class Aircraft {
var wings;
var engine;
}
Teraz mogę również wymienić silnik na bieżąco.
Musisz rzucić okiem na zasadę substytucji Liskowa w SOLIDNYCH zasadach projektowania klas wuja Boba . :)
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.
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.
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.
Mam trzy klasy, z których wszystkie wymagają wspólnej funkcjonalności. Więc jeśli napiszę klasę podstawową i wszystkie odziedziczą po niej, wszystkie będą miały tę funkcjonalność i będę musiał ją utrzymać tylko w jednym miejscu.
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.
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.
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.
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).
Podpisywanie jest właściwe i bardziej wydajne, gdzie niezmienniki można wyliczyć , w przeciwnym razie należy użyć kompozycji funkcji dla rozszerzenia.
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.
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):
class List {
data = new Array();
Integer size() {
return data.length;
}
add(Integer anInteger) {
data[data.length] = anInteger;
}
}
Następnie piszę Zbiór liczb całkowitych jako podklasę Listy liczb całkowitych:
class Set, inheriting from: List {
add(Integer anInteger) {
if (data.notContains(anInteger)) {
super.add(anInteger);
}
}
}
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 ).
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.
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.
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
class Vehicle{
protected double getPrice(){
// return price
}
}
class Car{
Vehicle vehicle;
protected double getPrice(){
return vehicle.getPrice();
}
}
Jak wiele osób powiedziało, zacznę od sprawdzenia, czy istnieje relacja „jest”. Jeśli istnieje, zwykle sprawdzam następujące elementy:
Określa, czy można utworzyć instancję klasy podstawowej. To znaczy, czy klasa podstawowa może być nieabstrakcyjna. Jeśli może być nieabstrakcyjne, zazwyczaj wolę kompozycję
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?
Główny powód używania dziedziczenia nie jest formą kompozycji - ma on na celu zachowanie polimorficzne. Jeśli nie potrzebujesz polimorfizmu, prawdopodobnie nie powinieneś używać dziedziczenia.
@MatthieuM. mówi w /software/12439/code-smell-inheritance-abuse/12448#comment303759_12448
Problem z dziedziczeniem polega na tym, że można go używać do dwóch celów ortogonalnych:
interfejs (dla polimorfizmu)
implementacja (do ponownego użycia kodu)
ODNIESIENIE
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.