Od: http://www.artima.com/lejava/articles/designprinciples4.html
Erich Gamma: Nadal uważam, że to prawda, nawet po dziesięciu latach. Dziedziczenie to fajny sposób na zmianę zachowania. Wiemy jednak, że jest kruchy, ponieważ podklasa może łatwo przyjmować założenia dotyczące kontekstu, w którym wywoływana jest metoda zastępowana. Istnieje ścisłe powiązanie między klasą podstawową a podklasą z powodu niejawnego kontekstu, w którym wywoływany jest kod podklasy, który podłączam. Kompozycja ma ładniejszą właściwość. Sprzęganie jest redukowane przez tylko kilka mniejszych rzeczy, które podłączasz do czegoś większego, a większy obiekt po prostu odsyła mniejszy obiekt. Z punktu widzenia API zdefiniowanie, że metodę można zastąpić, jest silniejszym zobowiązaniem niż zdefiniowanie, że można wywołać metodę.
Nie rozumiem o co mu chodzi. Czy ktoś mógłby to wyjaśnić?
Jeśli opublikujesz normalną funkcję, otrzymasz jednostronną umowę:
Co robi funkcja, jeśli zostanie wywołana?
Publikując oddzwanianie, udzielasz również jednostronnej umowy:
kiedy i jak będzie się nazywać?
A jeśli opublikujesz funkcję nadpisywalną, jest ona jednocześnie naraz, więc zawierasz dwustronną umowę:
Kiedy zostanie wywołana i co należy zrobić, jeśli zostanie wywołana?
Nawet jeśli użytkownicy nie nadużywają swoje API (łamiąc swoją część umowy, która może być drogie, aby wykryć), można łatwo zobaczyć, że te ostatnie potrzeby znacznie więcej dokumentacji, a wszystko Ci dokument jest zobowiązaniem, którego granice twoje dalsze wybory.
Przykładem reneging na taki dwustronny umowy jest ruch od
show
ihide
dosetVisible(boolean)
w java.awt.Component .źródło
show
ihide
nadal istnieją, są po prostu@Deprecated
. Tak więc zmiana nie psuje żadnego kodu, który je po prostu wywołuje. Ale jeśli je przesłoniłeś, nie zostaną wywołane przez klientów migrujących do nowego „setVisible”. (Nigdy nie korzystałem ze Swinga, więc nie wiem, jak często jest to nadpisywane; ale ponieważ zdarzyło się to dawno temu, wyobrażam sobie, że powodem, dla którego Deduplicator pamięta, jest to, że go boleśnie ugryzł.)Odpowiedź Kiliana Fotha jest doskonała. Chciałbym tylko dodać kanoniczny przykład *, dlaczego jest to problem. Wyobraź sobie całkowitą klasę Point:
Podklasujmy teraz, aby był punktem 3D.
Super proste! Wykorzystajmy nasze punkty:
Prawdopodobnie zastanawiasz się, dlaczego zamieszczam tak łatwy przykład. Oto haczyk:
Kiedy porównujemy punkt 2D z równoważnym punktem 3D, stajemy się prawdą, ale kiedy odwracamy porównanie, otrzymujemy fałsz (ponieważ p2a zawodzi
instanceof Point3D
).Wniosek
Zazwyczaj możliwe jest zaimplementowanie metody w podklasie w taki sposób, aby nie była już zgodna z tym, jak superklasa oczekuje, że zadziała.
Zasadniczo niemożliwe jest zaimplementowanie funkcji equals () w znacznie innej podklasie w sposób zgodny z jej klasą nadrzędną.
Kiedy piszesz klasę, którą zamierzasz pozwolić ludziom na podklasę, naprawdę dobrym pomysłem jest napisanie kontraktu dotyczącego tego, jak każda metoda powinna się zachowywać. Jeszcze lepszy byłby zestaw testów jednostkowych, które ludzie mogliby przeprowadzić przeciwko wdrożeniu zastąpionych metod, aby udowodnić, że nie naruszają umowy. Prawie nikt tego nie robi, bo to za dużo pracy. Ale jeśli cię to obchodzi, to jest to, co należy zrobić.
Doskonałym przykładem dobrze napisanej umowy jest komparator . Po prostu zignoruj to, o czym mówi
.equals()
z powodów opisanych powyżej. Oto przykład tego, jak Komparator może robić rzeczy,.equals()
których nie może .Notatki
Źródłem tego przykładu był „Efektywna Java” Josh Blocha, pozycja 8, ale Bloch używa ColorPoint, który dodaje kolor zamiast trzeciej osi i używa podwójnych zamiast ints. Przykład Java Blocha jest w zasadzie powielony przez Odersky / Spoon / Venners, którzy udostępnili swój przykład online.
Kilka osób sprzeciwiło się temu przykładowi, ponieważ jeśli poinformujesz klasę nadrzędną o podklasie, możesz rozwiązać ten problem. To prawda, jeśli istnieje wystarczająco mała liczba podklas i jeśli rodzic wie o nich wszystkich. Ale pierwotne pytanie dotyczyło stworzenia interfejsu API, dla którego ktoś inny napisze podklasy. W takim przypadku generalnie nie można zaktualizować implementacji nadrzędnej, aby była zgodna z podklasami.
Premia
Komparator jest również interesujący, ponieważ działa poprawnie w kwestii poprawnego implementowania funkcji equals (). Co więcej, jest to zgodne ze schematem naprawiania tego rodzaju problemu spadkowego: wzorzec projektowania strategii. Typy, którymi podekscytowani są ludzie Haskell i Scala, są również wzorem strategii. Dziedziczenie nie jest złe ani złe, jest po prostu trudne. Więcej informacji można znaleźć w artykule Philipa Wadlera Jak uczynić polimorfizm ad hoc mniej ad hoc
źródło
equals
jaki sposób Map i Set go definiują. Równość całkowicie ignoruje kolejność, w wyniku czego na przykład dwa SortedSets z tymi samymi elementami, ale różne kolejność sortowania wciąż się równa.B
jest dzieckiem klasyA
i powinna utworzyć instancję obiektu klasyB
, powinieneś być w stanie rzutowaćB
obiekt klasy na jego obiekt nadrzędny i korzystać z interfejsu API rzutowanej zmiennej bez utraty szczegółów implementacji dziecko. Złamałeś regułę, zapewniając trzecią właściwość. Jak planujesz uzyskać dostęp doz
współrzędnych po rzutowaniuPoint3D
zmiennejPoint2D
, gdy klasa podstawowa nie ma pojęcia, że taka właściwość istnieje? Jeśli rzucając klasę potomną do jej bazy, zepsujesz publiczny interfejs API, twoja abstrakcja będzie błędna.Dziedziczenie osłabia enkapsulację
Publikując interfejs z dozwolonym dziedziczeniem, znacznie zwiększasz rozmiar swojego interfejsu. Każdą nadpisywalną metodę można zastąpić, dlatego należy ją traktować jako wywołanie zwrotne dostarczane do konstruktora. Implementacja zapewniona przez twoją klasę jest jedynie domyślną wartością wywołania zwrotnego. W związku z tym należy przewidzieć pewien rodzaj umowy wskazującej oczekiwania wobec metody. Zdarza się to rzadko i jest głównym powodem, dla którego kod obiektowy nazywany jest kruchością.
Poniżej znajduje się prawdziwy (uproszczony) przykład z ram kolekcji java, dzięki uprzejmości Petera Norviga ( http://norvig.com/java-iaq.html ).
Co się stanie, jeśli podzielimy to na klasy?
Mamy błąd: od czasu do czasu dodajemy „pies”, a tablica hasztująca otrzymuje wpis „pies”. Przyczyną było to, że ktoś zapewnił implementację put, czego nie oczekiwał projektant klasy Hashtable.
Dziedziczenie przerywa rozszerzalność
Jeśli pozwolisz, aby twoja klasa została podklasowana, zobowiązujesz się nie dodawać żadnych metod do swojej klasy. W przeciwnym razie można to zrobić bez zepsucia czegokolwiek.
Gdy dodasz nowe metody do interfejsu, każdy, kto odziedziczył po twojej klasie, będzie musiał zaimplementować te metody.
źródło
Jeśli metoda ma zostać wywołana, musisz tylko upewnić się, że działa poprawnie. to jest to! Gotowy.
Jeśli metoda ma zostać zastąpiona, musisz również dokładnie przemyśleć jej zakres: jeśli zakres jest zbyt duży, klasa potomna często będzie musiała dołączyć wklejony kod z metody nadrzędnej; jeśli jest zbyt mały, wiele metod będzie musiało zostać zastąpionych, aby uzyskać pożądaną nową funkcjonalność - zwiększa to złożoność i niepotrzebną liczbę linii.
Dlatego twórca metody nadrzędnej musi poczynić założenia, w jaki sposób klasa i jej metody mogą zostać przesłonięte w przyszłości.
W cytowanym tekście autor mówi jednak o innym problemie:
Rozważ metodę,
a
która jest zwykle wywoływana z metodyb
, ale w niektórych rzadkich i nieoczywistych przypadkach z metodyc
. Jeśli autor nadrzędnej metody przeoczy tęc
metodę i jej oczekiwaniaa
, oczywiste jest, że coś może pójść nie tak.Dlatego ważniejsze
a
jest to, aby jasno i jednoznacznie zdefiniować, dobrze udokumentowane, „robi jedną rzecz i robi to dobrze” - bardziej niż gdyby była to metoda przeznaczona wyłącznie do wywołania.źródło