Dziedziczenie (programowanie)Dziedziczenie (ang. inheritance) – mechanizm współdzielenia funkcjonalności między klasami. Klasa może dziedziczyć po innej klasie, co oznacza, że oprócz deklaracji swoich własnych atrybutów oraz zachowań, uzyskuje także te pochodzące z klasy, z której dziedziczy. Klasa dziedzicząca jest nazywana klasą pochodną lub potomną (w j. angielskim: subclass lub derived class), zaś klasa, z której następuje dziedziczenie — klasą bazową (w ang. superclass). Z jednej klasy bazowej można uzyskać dowolną liczbę klas pochodnych. Klasy pochodne posiadają obok swoich własnych metod i deklaracji pól, również kompletny interfejs klasy bazowej. W językach programowania z prototypowaniem (np. JavaScript) nie występuje pojęcie klasy, dlatego dziedziczenie zachodzi tam pomiędzy poszczególnymi obiektami. Pojęcie dziedziczenia zostało wprowadzone po raz pierwszy przez twórców języka Simula[1]. Klasy bazowe i pochodneZależności między klasami bazowymi i pochodnymi tworzą tzw. hierarchię klas. Klasy pochodne otrzymują wszystkie metody i deklaracje atrybutów ze swoich klas bazowych oraz mogą dodawać nowe. Dopuszczalne jest także nadpisywanie istniejących metod, przy czym poszczególne języki programowania mogą żądać spełnienia dodatkowych warunków, np. pozostawienia niezmienionej listy argumentów wejściowych i typu wyniku. Wiele języków programowania umożliwia deklarowanie klas jako abstrakcyjnych. Nie można tworzyć obiektu klasy abstrakcyjnej, lecz można po takiej klasie dziedziczyć. Klasa abstrakcyjna może zawierać metody czysto wirtualne, które muszą zostać zaimplementowane przez klasy pochodne. Mechanizmu tego używa się, jeśli twórca klasy chce dostarczyć jedynie części funkcjonalności, tworząc szkielet dla innych, bardziej wyspecjalizowanych klas. W części języków programowania istnieje możliwość ograniczania widoczności dziedziczonych pól i metod:
Rodzaje dziedziczeniaW programowaniu obiektowym wyróżniane jest dziedziczenie pojedyncze oraz dziedziczenie wielokrotne. Z dziedziczeniem pojedynczym mamy do czynienia, gdy klasa pochodna dziedziczy po dokładnie jednej klasie bazowej (oczywiście klasa bazowa wciąż może dziedziczyć z jakiejś innej klasy), natomiast w dziedziczeniu wielokrotnym klas bazowych może być więcej. Wielokrotne dziedziczenie jest obsługiwane w takich językach, jak C++, Common Lisp czy Perl. Zwiększa możliwości ponownego wykorzystania kodu, lecz jednocześnie jest krytykowane za:
Powyższe problemy dotyczą przede wszystkim konfliktów implementacji. Dlatego nawet jeśli w danym języku programowania wielokrotne dziedziczenie klas jest niedozwolone, można je stosować w przypadku interfejsów, które mogą być traktowane, jak klasy abstrakcyjne zawierające wyłącznie metody czysto wirtualne. ZastosowaniaPodstawowym zastosowaniem dziedziczenia jest ponowne wykorzystanie kodu. Jeśli dwie klasy wykonują podobne zadania, możemy utworzyć dla nich wspólną klasę bazową, do której przeniesiemy definicje identycznych metod oraz deklaracje identycznych atrybutów. Ułatwi to testowanie oraz potencjalnie zwiększy niezawodność aplikacji w przypadku zmian. W razie ewentualnych problemów łatwiej będzie również odnaleźć przyczynę błędu. Dziedziczenie a polimorfizm (podtypowanie)Hierarchia klas może przekładać się na hierarchię typów. Możliwe jest wtedy podstawienie pod zmienną (lub atrybut funkcji) typu T obiektu typu S będącego podtypem T i dalsze używanie go jakby był typu T. Jest to możliwe dzięki temu, że podklasa posiada kompletny interfejs swojej nadklasy.
W podklasie może być zdefiniowana metoda już istniejąca w nadklasie. Konstrukcja taka umożliwia wykonywanie operacji na obiektach bez informacji, z jakim właściwie obiektem mamy do czynienia. Rozpatrzmy typową aplikację GUI wyświetlającą na ekranie różne komponenty (np. przycisk, pole tekstowe czy listę rozwijaną). Reagują one na te same zdarzenia: kliknięcie myszką, naciśnięcie klawisza, lecz każdy z nich reaguje inaczej, stosownie do tego czym jest. System obsługi zdarzeń najpierw określa, który z komponentów powinien obsłużyć zdarzenie, a następnie przekazuje mu je. Dzięki podtypowaniu opartym na dziedziczeniu możemy utworzyć wspólną klasę Decyzja o tym, która wersja zachowania zostanie wywołana w konkretnym miejscu, zależy od języka programowania i sposobu zdefiniowania metod. Rozpatrzmy następującą sytuację: class A {
method foo();
}
class B extends A {
method foo();
}
A obiektBazowy = new A();
B obiektPochodny = new B();
obiektBazowy.foo(); // 1
obiektBazowy = obiektPochodny;
obiektBazowy.foo(); // 2
Mamy klasę bazową
Jeśli zachodzi sytuacja druga, powiemy, że metoda W ogólnym ujęciu podtypowanie i dziedziczenie to dwa różne pojęcia. Dziedziczenie dotyczy powtórnego wykorzystania klasy bazowej, natomiast podtypowanie (polimorfizm) możliwości wykorzystania podtypu w miejscu nadtypu. OgraniczeniaDziedziczenie posiada kilka ograniczeń wynikających z faktu, że hierarchia klas jest ustalana w momencie kompilacji programu i nie może podlegać późniejszym zmianom. Wyobraźmy sobie klasę
Innym istotnym ograniczeniem jest uzależnienie kodu od konkretnej implementacji klasy, które może doprowadzić do błędów przy jej zmianie. Tego typu problem pojawia się zwłaszcza gdy dziedziczymy między klasami znajdującymi się w różnych komponentach (tzw. Problem kruchości klasy podstawowej)[2]. Rozwiązania alternatywneIstnieje kilka rozwiązań alternatywnych eliminujących poszczególne ograniczenia dziedziczenia. KompozycjaKompozycja polega na zastąpieniu dziedziczenia składaniem mniejszych obiektów. Posługując się dalej powyższym przykładem, możemy zostawić klasę Kompozycję można stosować w każdym języku obsługującym programowanie obiektowe. DomieszkiDomieszka pozwala uzyskać funkcjonalność podobną do wielokrotnego dziedziczenia, unikając jednocześnie trapiących je paradoksów. Jest to rodzaj klasy abstrakcyjnej, którą można „dodać” do właściwych klas. Klasa uzyskuje wszystkie deklaracje atrybutów oraz metody zdefiniowane w dodanych do niej domieszkach. Oddzielenie klas od domieszek pozwala wprowadzić jasne reguły rozwiązywania konfliktów. Przykładem języka wykorzystującego domieszki jest Ruby. Interfejsy i cechyInterfejs to rodzaj klasy abstrakcyjnej, która może zawierać wyłącznie metody czysto wirtualne oraz stałe. Ponieważ paradoksy dotyczą wyłącznie implementacji, której tu nie ma, w interfejsach można bezpiecznie korzystać z wielokrotnego dziedziczenia. Również klasy mogą implementować więcej niż jeden interfejs jednocześnie. Przykładami języków wykorzystujących interfejsy są Java oraz C#. Cechy umożliwiają wielokrotne wykorzystanie tego samego kawałka kodu w różnych klasach. W przeciwieństwie do domieszek, kod ten zachowuje się tak, jakby był zapisany w tych klasach bezpośrednio, a w momencie wykonania programu nie ma możliwości stwierdzenia czy dana metoda została zaimplementowana bezpośrednio w klasie czy dodana przez cechę. Zobacz teżPrzypisy
|