Skład zamiast dziedziczenia, ale

13

Próbuję nauczyć się inżynierii oprogramowania i napotykam sprzeczne informacje, które mnie dezorientują.

Uczyłem się OOP i czym są abstrakcyjne klasy / interfejsy i jak z nich korzystać, ale potem czytam, że należy „preferować kompozycję zamiast dziedziczenia”. Rozumiem, że skład ma miejsce, gdy jedna klasa tworzy / tworzy obiekt innej klasy w celu wykorzystania / interakcji z funkcjonalnością tego nowego obiektu.

Więc moje pytanie brzmi ... Czy mam nie używać abstrakcyjnych klas i interfejsów? Aby nie tworzyć klasy abstrakcyjnej i rozszerzać / dziedziczyć funkcji tej klasy abstrakcyjnej w konkretnych klasach, a zamiast tego po prostu komponować nowe obiekty, aby korzystać z funkcji drugiej klasy?

Czy też mam użyć kompozycji ORAZ odziedziczyć po klasach abstrakcyjnych; używasz ich obu w połączeniu ze sobą? Jeśli tak, czy mógłbyś podać kilka przykładów tego, jak to by działało i jakie są jego zalety?

Ponieważ czuję się najlepiej z PHP, używam go do ulepszania moich umiejętności OOP / SE przed przejściem do innych języków i przenoszeniem nowo nabytych umiejętności SE, więc przykłady użycia PHP byłyby bardzo mile widziane.

MikeMason
źródło
2
„Faworyzowanie kompozycji zamiast dziedziczenia” to głupi pomysł, który ma dokładnie tyle samo sensu, co „faworyzowanie piłek nad ćwiczeniami”. Są to dwa różne narzędzia, których można używać w dwóch różnych przypadkach, a czasy, w których można naprawdę używać jednego zamiennie, są w rzeczywistości dość rzadkie.
Mason Wheeler,
2
„Faworyzuj X nad Y” nie oznacza nigdy nie używaj Y, pomyśl tylko, czy X byłoby lepsze
Richard Tingle
2
„Preferuj kompozycję zamiast dziedziczenia” dokładniej wyraża się jako „Nigdy, nigdy, nigdy nie używaj dziedziczenia klas”, z wyjaśnieniem, że implementacja interfejsu nie jest dziedziczeniem i jeśli wybrany język obsługuje tylko klasy abstrakcyjne, a nie interfejsy, dziedziczenie ze 100% Klasa abstrakcyjna może być również postrzegana jako „nie dziedzicząca”.
David Arno,
1
@MasonWheeler, nie ma prawidłowych przypadków użycia dla dziedziczenia. Jest to co najmniej tak „zła” cecha, jak „goto”.
David Arno,
1
@DavidArno To po prostu śmieszne. Dziedziczenie jest jedną z najbardziej użytecznych, produktywnych funkcji, jakie kiedykolwiek opracowano w całej historii programowania. Podobnie jak w przypadku innych przydatnych rzeczy, istnieje wiele sposobów na nadużywanie go, ale właściwie użyte, znacznie zwiększa moc i wydajność pracy.
Mason Wheeler,

Odpowiedzi:

19

Składanie nad dziedziczeniem oznacza, że ​​jeśli chcesz ponownie użyć lub rozszerzyć funkcjonalność istniejącej klasy, często bardziej odpowiednie jest utworzenie innej klasy, która „zawinie” istniejącą klasę i użyje jej implementacji wewnętrznie. Wzór dekoratora jest tego przykładem.

Dziedziczenie nie jest dobrym domyślnym sposobem radzenia sobie ze scenariuszami ponownego użycia, ponieważ często chcesz użyć tylko części funkcji klasy podstawowej, a podklasa nie może obsługiwać całego kontraktu klasy podstawowej w sposób, który spełnia podstawienie Liskowa zasada .

Metoda szablonów jest dobrym przykładem przypadku, w którym dziedziczenie jest odpowiednie.

Zobacz tę odpowiedź, aby uzyskać wskazówki dotyczące wyboru między kompozycją a spadkiem.

astreltsov
źródło
+1 za wskazanie, że w przypadku częściowego ponownego wykorzystania klasy, niewykorzystana część jest bardziej czysto ignorowana w przypadku kompozycji niż w przypadku dziedziczenia.
Lawrence
4

Nie znam się na PHP, aby podać konkretne przykłady w tym języku, ale oto kilka wskazówek, które uznałem za przydatne.

Interfejsy / cechy są przydatne do definiowania zachowania w wielu klasach, które mogą mieć bardzo mało wspólnego.

Na przykład, jeśli pracujesz nad interfejsem JSON, możesz zdefiniować interfejs, który określa dwie metody: „toJson” i „toStatusCode”. Umożliwiłoby to napisanie pomocnika, który może wziąć dowolny obiekt implementujący ten interfejs i przekształcić go w odpowiedź HTTP.

W większości przypadków jest to lepsze niż dziedziczenie bezpośrednie, ponieważ jest mniej prawdopodobne, że cierpi z powodu delikatnego problemu z klasą podstawową, ponieważ dąży do płytkich hierarchii klas.

Bezpośrednie dziedziczenie najlepiej jest traktować jako coś, co robisz, gdy istnieją ku temu ważne powody, a nie coś, co robisz, chyba że istnieją ku temu ważne powody.

W językach, które nie obsługują domyślnych implementacji w interfejsach, rozsądnym sposobem może być współdzielenie zestawu podstawowych funkcji, ale zwykle mocno ogranicza to, co można zrobić z podklasami (wielokrotne dziedziczenie, jeśli jest dostępne, rzadko jest warte bólu) . Często uważam, że warto zaryzykować napisanie metod przekierowania na płytę kotłową, aby zamiast tego użyć kompozycji.

Kompozycja powinna być twoją domyślną strategią. W miarę możliwości komponuj z interfejsami i przesyłaj je jako argumenty konstruktora. To znacznie ułatwi Ci pisanie testów.

Unikaj pracy z globalnymi singletonami w jak największym stopniu, ponieważ porównywanie oczekiwanych i rzeczywistych danych wyjściowych jest właściwym problemem, jeśli część danych wyjściowych zależy od stanu zegara systemowego.

Nie zawsze jest to oczywiście możliwe. Na przykład platforma internetowa Play udostępnia bibliotekę JSON, która jest w zasadzie językiem specyficznym dla domeny. Zmiana tego byłaby dużym przedsięwzięciem, którego zastąpienie wezwań do „Json.prettyPrint” byłoby tylko bardzo niewielką częścią.

Morgen
źródło
1

Składanie ma miejsce, gdy klasa oferuje pewną funkcjonalność, tworząc instancję (być może wewnętrzną) klasy, która już implementuje tę funkcjonalność, zamiast dziedziczenia po tej klasie.

Na przykład, jeśli masz klasę, która modeluje statek, a powiedziano ci teraz, że twój statek powinien oferować lądowisko dla helikopterów, nie jest naturalne wyprowadzanie statku z lądowiska dla helikopterów (duh!), Zamiast tego powinieneś twój statek zawiera klasę lądowisk dla helikopterów i wystawiasz go jakąś Ship.getHelipad()metodą.

W dawnych latach (mniej więcej dziesięć lat temu) ludzie postrzegali dziedziczenie jako szybki i łatwy sposób na agregowanie funkcjonalności, więc było wiele przykładów rodzaju „statku dziedziczącego po lądowisku dla helikopterów”, które były oczywiście bardzo kiepskie.

Ale powiedzenie „Preferuj kompozycję nad dziedziczeniem” zostało starannie sformułowane, aby wyjaśnić, że jest to tylko sugestia, a nie reguła. Autor powiedzenia był na tyle ostrożny, by powstrzymać się od powiedzenia czegoś w rodzaju „ nigdy nie będziesz używać dziedzictwa, tylko kompozycji”. Zasadniczo zwraca to uwagę społeczności inżynierów oprogramowania na fakt, że dziedziczenie zostało nadużywane, podczas gdy w wielu przypadkach kompozycja zapewnia bardziej przejrzyste, eleganckie i łatwe do utrzymania projekty niż dziedziczenie.

Zasadniczo więc powiedzenie „Faworyzuj kompozycję nad dziedziczeniem” sugeruje, że ilekroć masz do czynienia z „dziedziczeniem czy komponowaniem”? pytanie, powinieneś mocno przemyśleć, jaka jest najbardziej odpowiednia strategia, i że istnieje największe prawdopodobieństwo, że najbardziej odpowiednią strategią okaże się kompozycja, a nie dziedziczenie.

Ale ponieważ nie jest to regułą, należy również pamiętać, że w wielu przypadkach dziedziczenie jest bardziej naturalne. Jeśli użyjesz kompozycji tam, gdzie powinieneś był użyć dziedziczenia, wiele zła spotka Twój kod.

Wracając do przykładu statku, jeśli twój statek musi oferować interfejs do interakcji z a FloatingMachine, bardziej naturalne jest wyprowadzenie go z FloatingMachineklasy abstrakcyjnej , która z kolei prawdopodobnie mogłaby pochodzić z innej Machineklasy abstrakcyjnej .

Oto ogólna zasada dla odpowiedzi na pytanie dotyczące składu a pytanie dotyczące dziedziczenia:

Czy moja klasa ma relację „jest” z interfejsem, który musi ujawnić? Jeśli tak, użyj dziedziczenia. Jeśli nie, użyj kompozycji.

Statek „to„ maszyna pływająca, a maszyna pływająca ”to„ maszyna. Tak więc dziedziczenie jest dla nich w porządku. Ale statek nie jest oczywiście lądowiskiem dla helikopterów. Lepiej więc skomponuj funkcjonalność lądowiska dla helikopterów.

Mike Nakis
źródło