Wolisz kompozycję niż dziedziczenie?


1603

Dlaczego wolisz kompozycję niż dziedziczenie? Jakie są kompromisy dla każdego podejścia? Kiedy wybrać dziedziczenie zamiast kompozycji?



40
Jest dobry artykuł na temat tego pytania tutaj . Moim osobistym zdaniem jest to, że nie ma „lepszej” lub „gorszej” zasady projektowania. Istnieje „odpowiedni” i „nieodpowiedni” projekt do konkretnego zadania. Innymi słowy - używam zarówno dziedzictwa, jak i składu, w zależności od sytuacji. Celem jest stworzenie mniejszego kodu, łatwiejszego do odczytania, ponownego użycia i ewentualnie dalszego rozszerzenia.
m_pGladiator,

1
w jednym zdaniu dziedziczenie jest jawne, jeśli masz metodę publiczną i zmienisz ją, zmieni to opublikowane API. jeśli masz kompozycję i obiekt został zmieniony, nie musisz zmieniać opublikowanego interfejsu API.
Tomer Ben David,

Odpowiedzi:


1188

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 .

    • np. dwupłatowiec Cessna odsłania pełny interfejs samolotu, jeśli nie więcej. Dzięki temu można czerpać z samolotu.
  • Czy TypeB chce tylko część / część zachowania ujawnionego przez TypeA? Wskazuje na potrzebę składu.

    • np. Ptak może potrzebować jedynie zachowania się samolotu w locie. W takim przypadku sensowne jest wyodrębnienie go jako interfejsu / klasy / obu i uczynienie go członkiem obu klas.

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?”


81
Drugi przykład pochodzi wprost z książki Head First Design Patterns ( amazon.com/First-Design-Patterns-Elisabeth-Freeman/dp/... ) :) Gorąco poleciłbym tę książkę każdemu, kto szukał tego pytania w Google.
Jeshurun

4
Jest to bardzo jasne, ale może coś przeoczyć: „Czy TypeB chce ujawnić pełny interfejs TypeA (wszystkie metody publiczne nie mniej), aby TypeB mógł być używany tam, gdzie oczekuje się TypeA?” Ale co jeśli to prawda, a TypeB ujawnia również pełny interfejs TypeC? A co jeśli TypeC nie został jeszcze wymodelowany?
Tristan

4
Nawiązujesz do tego, co moim zdaniem powinno być najbardziej podstawowym testem: „Czy ten obiekt może być wykorzystany przez kod, który oczekuje obiektów (co by było) typu podstawowego”. Jeśli odpowiedź brzmi „tak”, obiekt musi odziedziczyć. Jeśli nie, to prawdopodobnie nie powinno. Gdybym miał moje druty, języki zapewniłyby słowo kluczowe odnoszące się do „tej klasy” i zapewniły sposób na zdefiniowanie klasy, która powinna zachowywać się jak inna klasa, ale nie może być jej zastępowalna (taka klasa miałaby wszystko „to referencje klasy ”zamienione na siebie).
supercat,

22
@Alexey - chodzi o to, czy mogę przekazać dwupłatowiec Cessna wszystkim klientom, którzy oczekują samolotu, nie zaskakując ich? Jeśli tak, to są szanse, że chcesz dziedziczyć.
Gishu

9
Naprawdę staram się wymyślić przykłady, w których dziedziczenie byłoby moją odpowiedzią, często stwierdzam, że agregacja, kompozycja i interfejsy dają bardziej eleganckie rozwiązania. Wiele z powyższych przykładów można lepiej wyjaśnić przy użyciu tych podejść ...
Stuart Wakefield

414

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 .


107
To nie zawsze jest idealne podejście, to po prostu dobra wskazówka. zasada podstawienia Liskowa jest znacznie dokładniejsza (mniej zawodzi).
Bill K

40
„Mój samochód ma pojazd”. Jeśli uważasz to za osobne zdanie, a nie w kontekście programowania, nie ma to absolutnie żadnego sensu. I o to właśnie chodzi w tej technice. Jeśli brzmi to niezręcznie, prawdopodobnie jest źle.
Nick Zalutskiy

36
@Nick Sure, ale „My Car has a VehicleBehavior” ma większy sens (myślę, że twoja klasa „Vehicle” mogłaby nazywać się „VehicleBehavior”). Więc nie możesz opierać swojej decyzji na tym, że „ma„ vs ”to„ porównanie ”, musisz użyć LSP, albo popełnisz błędy
Tristan

35
Zamiast „myślenie o” zachowuje się jak ”. Dziedziczenie dotyczy dziedziczenia zachowania, a nie tylko semantyki.
ybakos

4
@ybakos „Zachowania się jak” można osiągnąć za pomocą interfejsów bez konieczności dziedziczenia. Z Wikipedii : „Implementacja kompozycji nad dziedziczeniem zazwyczaj rozpoczyna się od stworzenia różnych interfejsów reprezentujących zachowania, które system musi wykazywać… Zatem zachowania systemowe są realizowane bez dziedziczenia”.
DavidRR

210

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

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.

Kompozycja

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.

Skład nad dziedziczeniem

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.


6
Dziedziczenie: nie ma dwuznaczności. Wdrażasz klasę Manager w oparciu o wymagania. Zwróciłbyś więc „Managera operacji”, jeśli tak określiły twoje wymagania, w przeciwnym razie użyłbyś tylko implementacji klasy podstawowej. Możesz również uczynić Person klasą abstrakcyjną, a tym samym upewnić się, że klasy poboczne implementują właściwość Title.
Raj Rao,

68
Ważne jest, aby pamiętać, że można powiedzieć „Kompozycja nad dziedziczeniem”, ale to nie znaczy „Kompozycja zawsze nad dziedziczeniem”. „Jest a” oznacza dziedziczenie i prowadzi do ponownego użycia kodu. Pracownik jest osobą (pracownik nie ma osoby).
Raj Rao,

36
Przykład jest mylący. Pracownik jest osobą, dlatego powinien korzystać z dziedziczenia. Nie należy używać kompozycji w tym przykładzie, ponieważ jest to zły związek w modelu domeny, nawet jeśli technicznie można to zadeklarować w kodzie.
Michael Freidgeim

15
Nie zgadzam się z tym przykładem. Pracownik to Osoba, która jest podręcznikiem dotyczącym prawidłowego korzystania z dziedziczenia. Myślę też, że „problem” redefinicji pola Tytuł nie ma sensu. Fakt, że Employee.Title cienia Person.Title, jest oznaką złego programowania. W końcu są „Mr.” a „Manager operacji” naprawdę odnosi się do tego samego aspektu osoby (małe litery)? Zmieniłbym nazwę Employee.Title, dzięki czemu będę mógł odwoływać się do atrybutów Title i JobTitle pracownika, które mają sens w prawdziwym życiu. Co więcej, nie ma powodu dla Menedżera (ciąg dalszy ...)
Radon Rosborough

9
(... kontynuował) dziedziczyć zarówno od osoby, jak i od pracownika - w końcu pracownik już dziedziczy od osoby. W bardziej złożonych modelach, w których dana osoba może być menedżerem i agentem, prawdą jest, że można stosować wielokrotne dziedziczenie (ostrożnie!), Ale w wielu środowiskach byłoby wskazane posiadanie abstrakcyjnej klasy roli, z której menedżer (zawiera pracowników) on / ona zarządza) i agent (zawiera umowy i inne informacje) dziedziczy. Następnie pracownik jest osobą, która ma wiele ról. Zatem zarówno kompozycja, jak i dziedziczenie są właściwie stosowane.
Radon Rosborough,

141

Ze wszystkimi niezaprzeczalnymi korzyściami płynącymi z dziedziczenia, oto niektóre z jego wad.

Wady dziedziczenia:

  1. Nie można zmienić implementacji odziedziczonej po superklasach w czasie wykonywania (oczywiście ponieważ dziedziczenie jest definiowane w czasie kompilacji).
  2. Dziedziczenie naraża podklasę na szczegóły implementacji jej klasy nadrzędnej, dlatego często mówi się, że dziedziczenie przerywa enkapsulację (w tym sensie, że naprawdę musisz skupić się na interfejsach nie tylko na implementacji, więc ponowne użycie przez podklasę nie zawsze jest preferowane).
  3. Ścisłe sprzężenie zapewniane przez dziedziczenie powoduje, że implementacja podklasy jest bardzo związana z implementacją superklasy, że każda zmiana w implementacji nadrzędnej wymusi zmianę podklasy.
  4. Nadmierne ponowne użycie przez podklasę może sprawić, że stos dziedziczenia będzie bardzo głęboki i bardzo zagmatwany.

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.


5
Moim zdaniem jest to jedna z lepszych odpowiedzi - dodam do tego, że próba ponownego przemyślenia problemów związanych z kompozycją, z mojego doświadczenia, prowadzi do mniejszych, prostszych, bardziej samodzielnych, bardziej przydatnych klas , z jaśniejszym, mniejszym, bardziej ukierunkowanym zakresem odpowiedzialności. Często oznacza to mniejsze zapotrzebowanie na takie rzeczy jak wstrzykiwanie zależności lub kpiny (w testach), ponieważ mniejsze komponenty zwykle są w stanie samodzielnie działać. Po prostu moje doświadczenie. YMMV :-)
mindplay.dk

3
Ostatni akapit w tym poście naprawdę mnie kliknął. Dziękuję Ci.
Salx,

87

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.


81

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.


5
Właściwe narzędzie do właściwej pracy. Młot może być lepszy w uderzaniu niż klucz, ale to nie znaczy, że należy postrzegać klucz jako „gorszy młot”. Dziedziczenie może być pomocne, gdy rzeczy dodane do podklasy są niezbędne, aby obiekt zachowywał się jak obiekt nadklasy. Rozważmy na przykład klasę podstawową InternalCombustionEnginez klasą pochodną GasolineEngine. Ten ostatni dodaje rzeczy takie jak świece zapłonowe, których brakuje w klasie podstawowej, ale użycie tego jako InternalCombustionEnginespowoduje, że świece zapłonowe będą używane.
supercat

61

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


Dziedziczenie nie jest ani dobre, ani złe, to tylko specjalny przypadek Kompozycji. Gdzie rzeczywiście podklasa implementuje podobną funkcjonalność jak nadklasa. Jeśli proponowana podklasa nie jest ponownie wdrażana, a jedynie korzysta z funkcji tej nadklasy, oznacza to, że niepoprawnie użyto dziedziczenia. To błąd programisty, a nie refleksja na temat dziedziczenia.
iPherian,

42

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.


10
+1 Znalazłem coraz mniej, że dziedziczenie działa w większości sytuacji. Wolę współdzielone / dziedziczone interfejsy i kompozycję obiektów .... czy nazywa się to agregacją? Nie pytaj mnie, mam stopień EE !!
kenny

Uważam, że jest to najczęstszy scenariusz, w którym stosuje się „kompozycja nad dziedziczeniem”, ponieważ oba mogą pasować do teorii. Na przykład w systemie marketingowym możesz mieć pojęcie Client. Następnie PreferredClientpojawia się nowa koncepcja wyskakującego okienka. Czy powinien PreferredClientodziedziczyć 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?
plalx

Zamiast mieć różne rodzaje Clientklas, być może istnieje tylko jedna, która zawiera koncepcję AccountStandardAccountPreferredAccount
czegoś,

40

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

  1. Podpisywanie oznacza zgodność z podpisem typu (interfejsu), tj. Zestawem interfejsów API, i można zastąpić część podpisu, aby uzyskać polimorfizm podtypu.

  2. 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:

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

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

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

  4. Kompozycja ma funkcję odwracania kontroli, a jej zależność można wstrzykiwać dynamicznie, jak pokazano we wzorze dekoratora i wzorze zastępczym .

  5. Kompozycja ma zaletę programowania zorientowanego na kombinator , tzn. Działa w sposób podobny do wzorca złożonego .

  6. Kompozycja natychmiast następuje po zaprogramowaniu interfejsu .

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


34

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.


„Wiele osób uważa, że ​​dziedzictwo całkiem dobrze reprezentuje nasz prawdziwy świat, ale to nie jest prawda”. Tyle tego! W przeciwieństwie do niemal wszystkich samouczków programowania na świecie modelowanie obiektów rzeczywistych jako łańcuchów dziedziczenia jest prawdopodobnie złym pomysłem na dłuższą metę. Powinieneś używać dziedziczenia tylko wtedy, gdy istnieje niewiarygodnie oczywisty, wrodzony, prosty związek. Like TextFileis a File.
neonblitzer

25

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


25

Dlaczego wolisz kompozycję niż dziedziczenie?

Zobacz inne odpowiedzi.

Kiedy możesz skorzystać z dziedziczenia?

Często mówi się, że klasa Barmoże odziedziczyć klasę, Foogdy prawdziwe jest następujące zdanie:

  1. bar to głupek

Niestety sam powyższy test nie jest wiarygodny. Zamiast tego użyj następujących opcji:

  1. bar to głupek ORAZ
  2. bary mogą zrobić wszystko, co potrafią piłkarzyki.

Pierwsze testy, zapewnia, że wszystkie getters o Foosens w Bar(= wspólne właściwości), natomiast drugie badanie daje pewność, że wszystkie setery o Foosens 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.

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 Widgetw ramach GUI lub klasy polimorficznej Nodew 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 :

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


Scenariusze elipsy koła i kwadratu-prostokąta są złymi przykładami. Podklasy są niezmiennie bardziej złożone niż ich nadklasy, więc problem został wymyślony. Ten problem rozwiązano przez odwrócenie relacji. Elipsa pochodzi z koła, a prostokąt z kwadratu. Używanie kompozycji w tych scenariuszach jest bardzo głupie.
Fuzzy Logic,

@FuzzyLogic Zgadzam się, ale w rzeczywistości mój post nigdy nie opowiada się za użyciem kompozycji w tym przypadku. Powiedziałem tylko, że problem elipsy jest doskonałym przykładem tego, dlaczego „is-a” nie jest dobrym testem, aby stwierdzić, że Circle powinien wywodzić się z Ellipse. Gdy wyciągniemy wniosek, że tak naprawdę Circle nie powinien wywodzić się z Ellipse z powodu naruszenia LSP, wówczas możliwe opcje to odwrócenie relacji, użycie kompozycji lub użycie klas szablonów lub zastosowanie bardziej złożonego projektu obejmującego dodatkowe klasy lub funkcje pomocnicze, itd ... i decyzja powinna oczywiście zostać podjęta indywidualnie.
Boris Dalstein,

1
@FuzzyLogic A jeśli jesteście ciekawi, co zalecałbym w konkretnym przypadku Circle-Ellipse: zalecałbym nie wdrażanie klasy Circle. Problem z odwracaniem relacji polega na tym, że narusza ona także LSP: wyobraź sobie funkcję 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.
Boris Dalstein,

computeArea(Circle *c) { return pi * width * height / 4.0; }Teraz jest ogólny.
Fuzzy Logic

2
@FuzzyLogic Nie zgadzam się: zdajesz sobie sprawę, że oznacza to, że Krąg klasy przewidział istnienie pochodnej klasy Ellipse, a zatem pod warunkiem 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.
Boris Dalstein,

19

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


15

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


3
Warto zauważyć, że dziedziczenie nie jest jedynym sposobem na osiągnięcie polimorfizmu. Wzór dekoratora zapewnia wygląd polimorfizmu poprzez kompozycję.
BitMask777,

1
@ BitMask777: Polimorfizm podtypu jest tylko jednym rodzajem polimorfizmu, innym byłby polimorfizm parametryczny, do tego nie potrzebujesz dziedziczenia. Co ważniejsze: mówiąc o dziedziczeniu, chodzi o dziedziczenie klas; .ie możesz mieć polimorfizm podtypu, mając wspólny interfejs dla wielu klas, i nie masz problemów z dziedziczeniem.
egaga

2
@engaga: Zinterpretowałem twój komentarz Inheritance is sometimes useful... That way you can have polymorphismjako 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.
BitMask777

15

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 Enginenaraża mutatora na zmianę jego
właściwości, albo przeprojektowuję Aircraftjako:

Class Aircraft {
  var wings;
  var engine;
}

Teraz mogę również wymienić silnik na bieżąco.


Twój post porusza kwestię, której wcześniej nie brałem pod uwagę - aby kontynuować analogię obiektów mechanicznych z wieloma częściami, na czymś takim jak broń palna, na ogół jest jedna część oznaczona numerem seryjnym, którego numer seryjny uważa się za broni palnej jako całości (w przypadku pistoletu zwykle byłaby to rama). Można wymienić wszystkie pozostałe części i nadal mieć tę samą broń palną, ale jeśli rama pęknie i będzie wymagała wymiany, wynikiem montażu nowej ramy ze wszystkimi pozostałymi częściami oryginalnego pistoletu byłaby nowa broń. Zauważ, że ...
supercat,

... fakt, że wiele części broni może mieć oznaczone numery seryjne, nie oznacza, że ​​broń może mieć wiele tożsamości. Tylko numer seryjny na ramie identyfikuje pistolet; numer seryjny na jakiejkolwiek innej części określa, z jakim pistoletem zostały wyprodukowane te części, które mogą nie być pistoletem, z którym zostały zmontowane w danym momencie.
supercat,


7

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.


7

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:

  1. Masz wiele klas z tym samym interfejsem i chcesz zaoszczędzić czas na ich pisanie
  2. Musisz użyć tej samej klasy bazowej dla każdego obiektu
  3. Musisz zmodyfikować zmienne prywatne, które w żadnym wypadku nie mogą być publiczne

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.


7

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:

  • Odkrywamy, że istnieją inne funkcje, które powinny mieć nasze klasy. Jeśli sposób, w jaki dodajemy funkcjonalność do klas, polega na dziedziczeniu, musimy zdecydować - czy dodamy ją do istniejącej klasy podstawowej, nawet jeśli nie każda klasa, która dziedziczy po niej, potrzebuje tej funkcjonalności? Czy tworzymy kolejną klasę podstawową? Ale co z klasami, które już dziedziczą po innej klasie bazowej?
  • Odkrywamy, że dla jednej z klas dziedziczących po naszej klasie podstawowej chcemy, aby klasa podstawowa zachowywała się nieco inaczej. Więc teraz wracamy i majstrujemy przy naszej klasie bazowej, może dodając jakieś wirtualne metody, lub nawet gorzej, jakiś kod, który mówi: „Jeśli jestem dziedziczony typu A, zrób to, ale jeśli jestem dziedziczony typu B, zrób to . ” To źle z wielu powodów. Po pierwsze, za każdym razem, gdy zmieniamy klasę podstawową, skutecznie zmieniamy każdą dziedziczoną klasę. Tak więc naprawdę zmieniamy klasy A, B, C i D, ponieważ potrzebujemy nieco innego zachowania w klasie A. Choć jesteśmy tak ostrożni, możemy złamać jedną z tych klas z powodów, które nie mają z tym nic wspólnego zajęcia
  • Być może wiemy, dlaczego postanowiliśmy sprawić, aby wszystkie te klasy dziedziczyły po sobie, ale może to nie (prawdopodobnie nie będzie) mieć sensu dla kogoś innego, kto musi utrzymywać nasz kod. Możemy zmusić ich do trudnego wyboru - czy zrobię coś naprawdę brzydkiego i niechlujnego, aby dokonać potrzebnej mi zmiany (zobacz poprzedni punkt), czy też po prostu przepisuję kilka z nich.

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.


To, że klasy nie mogą korzystać z wielu klas podstawowych, nie jest złym rozważaniem na temat dziedziczenia, ale raczej złym rozważaniem na temat braku możliwości danego języka.
iPherian,

Od czasu napisania tej odpowiedzi przeczytałem ten post z „Wujka Boba”, który dotyczy tego braku możliwości. Nigdy nie korzystałem z języka, który pozwala na wielokrotne dziedziczenie. Ale patrząc wstecz, pytanie jest oznaczone jako „język agnostyczny”, a moja odpowiedź zakłada C #. Muszę poszerzyć swoje horyzonty.
Scott Hannen,

6

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.


6

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.


Powiedziałeś, że dziedzictwo i dziedzictwo. nie masz na myśli dziedziczenia i kompozycji?
trevorKirkby

Nie, ty nie. Możesz równie dobrze zdefiniować psi interfejs i pozwolić, aby każdy pies go wdrożył, a skończysz z większym kodem SOLID.
markus

5

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


+1 za wzmiankę o interfejsie. Często używam tego podejścia, aby ukryć istniejące klasy i sprawić, by moja nowa klasa była odpowiednio testowana jednostkowo przez wyśmiewanie obiektu używanego do kompozycji. Wymaga to od właściciela nowego obiektu przekazania klasy kandydującej nadrzędnej.
Kell


4

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

  • Czy twoja klasa jest częścią struktury korzystającej z polimorfizmu? Na przykład, jeśli masz klasę Shape, która deklaruje metodę o nazwie draw (), wtedy wyraźnie potrzebujemy klas Circle i Square, aby były one podklasami Shape, tak aby ich klasy klientów zależały od Shape, a nie od konkretnych podklas.
  • Czy Twoja klasa musi ponownie użyć interakcji wysokiego poziomu zdefiniowanych w innej klasie? Metoda szablon wzór projektu byłoby niemożliwe do wykonania bez dziedziczenia. Wierzę, że wszystkie rozszerzalne ramy używają tego wzorca.

Jeśli jednak intencją jest wyłącznie ponowne użycie kodu, wówczas najprawdopodobniej kompozycja jest lepszym wyborem projektowym.


4

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


3

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.


2

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.


2

Chociaż preferowana jest Kompozycja, chciałbym zwrócić uwagę na zalety Dziedzictwa i wady Kompozycji .

Zalety dziedziczenia:

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

  2. Dzięki dziedziczeniu możesz definiować / modyfikować / rozszerzać możliwości

    1. Klasa podstawowa nie zapewnia implementacji, a podklasa musi zastąpić pełną metodę (abstrakt) => Możesz wdrożyć umowę
    2. Klasa podstawowa zapewnia domyślną implementację, a podklasa może zmienić zachowanie => Możesz ponownie zdefiniować kontrakt
    3. Podklasa dodaje rozszerzenie do implementacji klasy podstawowej, wywołując super.methodName () jako pierwszą instrukcję => Możesz przedłużyć umowę
    4. Klasa podstawowa określa strukturę algorytmu, a podklasa zastąpi część algorytmu => Metodę szablonową można zaimplementować bez zmiany szkieletu klasy podstawowej

Wady składu:

  1. W dziedziczeniu podklasa może bezpośrednio wywoływać metodę klasy bazowej, nawet jeśli nie implementuje metody klasy bazowej z powodu IS A. relacji. Jeśli używasz kompozycji, musisz dodać metody do klasy kontenera, aby wyświetlić API klasy zawartej

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

Myślę, że to nie odpowiada na pytanie
almanegra

Możesz ponownie spojrzeć na pytanie OP. Odniosłem się: jakie są kompromisy dla każdego podejścia?
Ravindra babu

Jak już wspomniałeś, mówisz tylko o „zaletach dziedziczenia i wadach kompozycji”, a nie o kompromisach w stosunku do KAŻDEGO podejścia lub o przypadkach, w których powinieneś stosować jeden nad drugim
almanegra

za i przeciw zapewnia kompromis, ponieważ wady dziedziczenia to wady składu, a wady kompozycji to wady dziedziczenia.
Ravindra babu

1

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

  1. Która klasa jest lepsza?
  2. Dziedziczenie a agregacja

1
Nie jestem pewien, dlaczego „klasa podstawowa jest abstrakcyjna?” dane do dyskusji .. LSP: czy wszystkie funkcje działające na psach będą działać, jeśli zostaną przekazane obiekty Pudla? Jeśli tak, to Pudel można zastąpić Psem, a zatem może dziedziczyć po Psie.
Gishu

@Gishu Thanks. Na pewno zajrzę do LSP. Ale zanim to możliwe, proszę podać „przykład, w którym dziedziczenie jest właściwe, w którym klasa podstawowa nie może być abstrakcyjna”. Moim zdaniem dziedziczenie ma zastosowanie tylko wtedy, gdy klasa podstawowa jest abstrakcyjna. Jeśli klasa podstawowa musi zostać utworzona oddzielnie, nie należy dziedziczyć. Oznacza to, że chociaż księgowy jest pracownikiem, nie używaj dziedziczenia.
LCJ,

1
ostatnio czytam WCF. Przykładem .NET Framework jest SynchronizationContext (można utworzyć instancję base +), które kolejki działają w wątku ThreadPool. Derywacje obejmują WinFormsSyncContext (kolejka do wątku interfejsu użytkownika) i DispatcherSyncContext (kolejka do dyspozytora WPF)
Gishu

@Gishu Thanks. Przydałoby się jednak scenariusz oparty na domenie Banku, domenie HR, domenie detalicznej lub innej popularnej domenie.
LCJ,

1
Przepraszam. Nie znam tych domen. Innym przykładem, jeśli poprzednia była zbyt tępa, jest klasa Control w Winforms / WPF. Można utworzyć instancję podstawowej / ogólnej kontroli. Derywacje obejmują pola listy, pola tekstowe itp. Teraz, gdy o tym myślę, wzór Dekoratora projektu jest dobrym przykładem IMHO i również przydatny. Dekorator wywodzi się z nieabstrakcyjnego obiektu, który chce owinąć / ozdobić.
Gishu,

1

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.


1
D dziedziczy B i C nie A. Jeśli tak, to użyłby implementacji X, która jest w klasie A.
Fabricio

1
@fabricio: dzięki, edytowałem tekst. Nawiasem mówiąc, taki scenariusz nie może wystąpić w językach, które nie pozwalają na dziedziczenie wielu klas, prawda?
Veverke

tak, masz rację .. i nigdy nie pracowałem z takim, który pozwala na wielokrotne dziedziczenie (jak na przykładzie problemu z diamentami) ..
fabricio
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.