czwartek, 7 sierpnia 2008

Telefonistka dziedziczy po telefonie

"I once attended a Java user group meeting where James Gosling (Java's inventor) was the featured speaker. During the memorable Q&A session, someone asked him: "If you could do Java over again, what would you change?" "I'd leave out classes," he replied. After the laughter died down, he explained that the real problem wasn't classes per se, but rather implementation inheritance (the extends relationship). Interface inheritance (the implements relationship) is preferable. You should avoid implementation inheritance whenever possible."

"The extends keyword is evil; maybe not at the Charles Manson level, but bad enough that it should be shunned whenever possible."

- tako rzecze Allen Holub - ekstremista obiektowy.


Odpowiedź na poprzedniego posta skłoniła mnie do naskrobania paru słów o dziedziczeniu w ogóle...

Dużo pisze się o tym, że dziedziczenie jest źle rozumiane, nadużywane czy używane w nieodpowiedni sposób. Można zadać pytanie: o co w ogóle chodzi? Przecież pani w szkole mówiła, że to podstawa OO;)

Relacja dziedziczenia pomiędzy klasami jest najsilniejszym rodzajem powiązania (coupling) co w oczywisty sposób prowadzi do problemu "kruchej klasy bazowej". Zmiany w klasie bazowej mają oczywiście drastyczny wpływ na kod systemu, który po niej dziedziczy. Projektowanie każdej takiej klasy bazowej to zestaw decyzji niemal strategicznych, a jej zmiany to i tak zazwyczaj katastrofa.

Poza tym oczywiście klasa dziedzicząca ciągnie za sobą (jak przykładowy smród za gaciami) bagaż klasy bazowej. Jak słusznie zauważył Paweł w OO nie ma pól i metod regresywnych:) W OO Dziedziczymy po przodku wszystko a nie tylko co jest przydatne - nawet pola prywatne... niby ich nie ma a w pamięci są;)

Dziedziczenie jest złym sposobem na "wyciąganie przed nawias" wspólnych elementów. Lepiej wspólne części hermetyzować do osobnych klas i następnie je agregować/komponować.

Osobiście kieruję się Zasadą Podstawienia Liskov: "Let q(x) be a property provable about objects x of type T. Then q(y) should be true for objects y of type S where S is a subtype of T."
hehe, ten pseudonaukowy-turbo-bełkot można w skrócie ująć tak: używaj dziedziczenia tylko wtedy, gdy będziesz korzystał z polimorfizmu. I to się sprawdza. Od kiedy staram sie trzymać tej zasady wiele problemów z rozbudową i zmianami po prostu już się nie pojawia. Zamiast dziedziczenia zazwyczaj stosuję kompozycję strategii, która jak mi się wydaje jest "metawzorcem" - opiera się na nim duża część innych wzorców - różny jest jedynie kontekst (ale o tym napiszę w przyszłości).

//=========================
"Klasa telefon i klasa telefonistka, która dziedziczy po telefonie" - taką odpowiedź usłyszałem podczas rozmowy rekrutacyjnej od "młodego, zdolnego i dobrze zapowiadającego się kandydata", na pytanie o jakiś nieksiążkowy przykład dziedziczenia.
Nie przeszedł rekrutacji...

2 komentarze:

Anonimowy pisze...

Kiedyś wydawało mi się że dziedziczenie jest dobre, przydatne i fajne. Z perpektywy czasu widzę że mam coraz mniej miejsc aby je wykorzytstać. W większości przypadków prsta agregacja/kompozyja jest przejrzysta i elegancka.

Tak jak wspomniałeś, struktura klasy bazowwej to strategiczna decyzja, i uwzględniająć złożoność każdego większego projektu potencjalne zyski płynące z zastosowania dziedziczenia są dużo mniejsze niż straty (np w konsekwencji błędnej analizy). Skupienie się na zachowaniu (interfejsach), a nie strukturze obiektów pozwala poprawniej zamodelować problem.

Biedna telefonistka dziedziczy po telefonie ? No cóż, jak by szanowny wykładowca wspomnianego kandydata, wyszedł z epoki Pascala łupanego...

Sławek Sobótka pisze...

Dziedziczenia używam zwykle tam, gdzie klasa ma dokładnie określoną jedną odpowiedzialność. Czyli przykładowo w strategiach...

Weźmy szkolny przykład, który jest najczęściej wypaczany: figury. Jeżeli mamy fogurę, która potrafi się narysować, policzyć swe pole itp to rysowanie powinniśmy wyenkapsulować do strategii rysowania (nazwijmy ją renderer), i to dopiero na poziomie renderea mogę bawić się w dziedziczenie po sobie jakiś renderów.

Dzięki temu:
- trzymam się LSP - rendery są używane polimorficznie, nie mam problemu (o ile projekt jest dobry)
- z niepotrzebnym bagażem klas bazowych - bo renderery są skupione na wspólnym koherentnym problemie
- problem kruchej klasy bazowej jest przynajmniej odsunięty poza stabilny interfejs renderea.

Z bardziej ogólnej perspektywy: budując nano-frameworki oparte na interfejsach możemy pozwolić sobie na dostarczanie hierarchii klas jako implementacji niektórych wyspecjalizowanych interfejsów.