Dlaczego warto unikać dziedziczenia Java „Rozszerza”

40

Powiedział Jame Gosling

„W miarę możliwości należy unikać dziedziczenia implementacji.”

i zamiast tego użyj dziedziczenia interfejsu.

Ale dlaczego? Jak możemy uniknąć dziedziczenia struktury obiektu za pomocą słowa kluczowego „extends”, a jednocześnie uczynić nasz kod obiektowym zorientowanym?

Czy ktoś mógłby podać obiektowy przykład ilustrujący tę koncepcję w scenariuszu „zamawianie książki w księgarni”?

Nowicjusz
źródło
Problem polega na tym, że możesz rozszerzyć tylko jedną klasę.

Odpowiedzi:

38

Gosling nie powiedział, że powinien unikać użycia słowa kluczowego extends ... po prostu powiedział, aby nie używać dziedziczenia jako sposobu na ponowne użycie kodu.

Dziedziczenie nie jest złe samo w sobie i jest bardzo potężnym (niezbędnym) narzędziem do tworzenia struktur OO. Jednak gdy nie jest używany właściwie (to jest używany do czegoś innego niż tworzenie struktur obiektowych), tworzy kod, który jest bardzo ściśle powiązany i bardzo trudny w utrzymaniu.

Ponieważ do tworzenia struktur polimorficznych powinno się używać dziedziczenia, można to zrobić równie łatwo za pomocą interfejsów, stąd instrukcja projektowania interfejsów zamiast klas, podczas projektowania struktur obiektów. Jeśli okaże się, że używasz rozszerzeń jako sposobu na uniknięcie wklejania kodu z jednego obiektu do drugiego, być może używasz go nieprawidłowo i lepiej byłoby skorzystać z wyspecjalizowanej klasy do obsługi tej funkcji i zamiast tego udostępnić ten kod poprzez kompozycję.

Przeczytaj o zasadzie substytucji Liskowa i zrozum ją. Ilekroć masz zamiar przedłużyć klasę (lub interfejs w tym zakresie), zadaj sobie pytanie, czy naruszasz zasadę, jeśli tak, to najprawdopodobniej (ab) używasz dziedziczenia w nienaturalny sposób. Jest to łatwe do zrobienia i często wydaje się właściwe, ale utrudni utrzymanie, testowanie i ewolucję kodu.

--- Edytuj Ta odpowiedź stanowi całkiem dobry argument, aby odpowiedzieć na pytanie bardziej ogólnie.

Newtopian
źródło
3
Dziedziczenie (implementacja) nie jest niezbędne do tworzenia struktur OO. Bez niego możesz mieć język OO.
Andres F.
Czy możesz podać przykład? Nigdy nie widziałem OO bez dziedzictwa.
Newtopian
1
Problem polega na tym, że nie ma przyjętej definicji tego, czym jest OOP. (Nawiasem mówiąc, nawet Alan Kay zaczął żałować wspólnej interpretacji swojej definicji, z jej obecnym naciskiem na „przedmioty” zamiast „wiadomości”). Oto próba odpowiedzi na twoje pytanie . Zgadzam się, że rzadkie jest oglądanie OOP bez dziedziczenia; stąd mój argument polegał jedynie na tym, że nie jest to konieczne ;)
Andres F.
9

We wczesnych dniach programowania obiektowego dziedziczenie implementacji było złotym młotem ponownego wykorzystania kodu. Jeśli w jakiejś klasie potrzebna była funkcjonalność, należało ją publicznie odziedziczyć po tej klasie (były to czasy, gdy C ++ był bardziej rozpowszechniony niż Java), a ta funkcjonalność została „za darmo”.

Tyle że nie było tak naprawdę za darmo. W rezultacie powstały niezwykle zawiłe hierarchie dziedziczenia, które były prawie niemożliwe do zrozumienia, w tym drzewa spadkowe w kształcie rombu, które były trudne w obsłudze. Jest to jeden z powodów, dla których projektanci Java postanowili nie obsługiwać wielokrotnego dziedziczenia klas.

Rady Goslinga (i inne stwierdzenia), jakoby były silnym lekarstwem na dość poważną chorobę, i możliwe jest, że niektóre dzieci zostaną wyrzucone z kąpielą. Nie ma nic złego w korzystaniu z dziedziczenia do modelowania relacji między klasami, która naprawdę jest is-a. Ale jeśli chcesz po prostu skorzystać z funkcji innej klasy, lepiej najpierw sprawdź inne opcje.

JohnMcG
źródło
6

Dziedzictwo zyskało ostatnio dość przerażającą reputację. Jednym z powodów, aby tego uniknąć, jest zasada projektowa „kompozycja nad dziedziczeniem”, która zasadniczo zachęca ludzi do nieużywania dziedziczenia tam, gdzie nie jest to prawdą, tylko po to, aby zaoszczędzić trochę pisania kodu. Ten rodzaj dziedziczenia może powodować trudne do znalezienia i trudne do naprawienia błędy. Powodem, dla którego twój przyjaciel prawdopodobnie sugerował użycie interfejsu, jest to, że interfejsy zapewniają bardziej elastyczne i łatwe w utrzymaniu rozwiązanie dla rozproszonych API. Częściej pozwalają na dokonywanie zmian w interfejsie API bez łamania kodu użytkownika, gdzie ujawnianie klas często łamie kod użytkownika. Najlepszym przykładem tego w .Net jest różnica w użyciu pomiędzy List a IList.

Morgan Herlocker
źródło
5

Ponieważ możesz rozszerzyć tylko jedną klasę, a jednocześnie wdrożyć wiele interfejsów.

Kiedyś słyszałem, że dziedziczenie określa, kim jesteś, ale interfejsy określają rolę, którą możesz odegrać.

Interfejsy pozwalają również na lepsze wykorzystanie polimorfizmu.

To może być część tego powodu.

Mahmoud Hossam
źródło
dlaczego głosowanie negatywne?
Mahmoud Hossam,
6
Odpowiedź stawia wózek przed koniem. Java pozwala rozszerzyć tylko jedną klasę ze względu na przegubowe zasady Gosling (projektant Java). To tak, jakby powiedzieć, że dzieci powinny nosić kaski podczas jazdy na rowerze, ponieważ takie jest prawo. To prawda, ale zarówno prawo, jak i rada wynikają z unikania obrażeń głowy.
JohnMcG
@JohnMcG Nie wyjaśniam wyboru projektu Gosling, po prostu udzielam porady programistom, którzy zwykle nie zawracają sobie głowy projektowaniem języka.
Mahmoud Hossam
1

Nauczono mnie również tego i wolę interfejsy tam, gdzie to możliwe (oczywiście nadal używam dziedziczenia tam, gdzie ma to sens).

Myślę, że jedną rzeczą jest oddzielenie kodu od konkretnych implementacji. Powiedzmy, że mam klasę o nazwie ConsoleWriter, która zostaje przekazana do metody napisania czegoś i zapisuje to na konsoli. Powiedzmy teraz, że chcę przejść do drukowania do okna GUI. Cóż, teraz muszę zmodyfikować metodę lub napisać nową, która przyjmuje GUIWriter jako parametr. Jeśli zacznę od zdefiniowania interfejsu IWriter i jeśli metoda przyjmie IWriter, mógłbym zacząć od ConsoleWriter (który implementuje interfejs IWriter), a następnie napisać nową klasę o nazwie GUIWriter (która również implementuje interfejs IWriter), a następnie I musiałby po prostu wyłączyć zaliczaną klasę.

Inną rzeczą (co jest prawdą w języku C #, co do Javy nie jest pewne) jest to, że można rozszerzyć tylko 1 klasę, ale zaimplementować wiele interfejsów. Powiedzmy, że miałem zajęcia o nazwie Teacher, MathTeacher i HistoryTeacher. Teraz MathTeacher i HistoryTeacher wychodzą od Nauczyciela, ale co, jeśli chcemy, aby klasa reprezentowała kogoś, kto jest zarówno Nauczycielem matematyki, jak i Nauczycielem historii. Może stać się dość niechlujny, gdy próbujesz odziedziczyć po wielu klasach, gdy możesz to zrobić tylko pojedynczo (istnieją sposoby, ale nie są one dokładnie optymalne). Dzięki interfejsom możesz mieć 2 interfejsy o nazwie IMathTeacher i IHistoryTeacher, a następnie mieć jedną klasę wychodzącą od Nauczyciela i implementującą te 2 interfejsy.

Jedną wadą korzystania z interfejsów jest to, że czasami widzę, jak ludzie duplikują kod (ponieważ musisz utworzyć implementację dla każdej klasy, implementować interfejs), jednak istnieje czyste rozwiązanie tego problemu, na przykład użycie rzeczy takich jak delegaci (nie jestem pewien czym jest odpowiednik Java).

Myślą, że największym powodem używania interfejsów do dziedziczenia jest oddzielenie kodu implementacyjnego, ale nie sądzę, że dziedziczenie jest złe, ponieważ wciąż jest bardzo przydatne.

ryanzec
źródło
2
„możesz rozszerzyć tylko 1 klasę, ale zaimplementować wiele interfejsów” Dotyczy to zarówno Java, jak i C #.
FrustratedWithFormsDesigner
Jednym z możliwych rozwiązań problemu duplikacji kodu jest utworzenie klasy abstrakcyjnej, która implementuje interfejs i rozszerza go. Używaj ostrożnie; widziałem tylko kilka przypadków, że ma to sens.
Michael K,
0

Jeśli odziedziczysz po klasie, bierzesz zależność nie tylko od tej klasy, ale musisz także honorować wszelkie ukryte umowy utworzone przez klientów tej klasy. Wujek Bob stanowi świetny przykład tego, jak łatwo subtelnie naruszać Zasadę Zastępstwa Liskowa za pomocą podstawowego kwadratu rozszerzającego dylemat Prostokąt . Interfejsy, ponieważ nie mają implementacji, również nie mają ukrytych umów.

Michael Brown
źródło
2
Jedno nitpicking: problem z elipsą koła będzie nadal występował, nawet jeśli zostaną użyte tylko czyste interfejsy, jeśli obiekty mają być zmienne.
rwong