Dziedziczenie jest jednym z podstawowych mechanizmów obiektowości, chętnie stosowanym przez projektantów i programistów. Zobaczmy więc, na czym polega mechanizm dziedziczenia i przyjrzyjmy się sytuacjom, gdy jego użycie nie jest najszczęśliwszym rozwiązaniem.

Dziedziczenie jest szczególnego rodzaju zależnością pomiędzy dwiema klasami na diagramach klas. Określa ono, że jedna z tych klas (zwana podklasą lub specjalizacją) jest szczególnym przypadkiem drugiej klasy (zwanej nadklasą lub generalizacją). Na rysunku 1. jest przedstawiony prosty przykład użycia dziedziczenia. Zgodnie z nim, koło i kwadrat są szczególnymi przypadkami figury. Inaczej mówiąc, każdy obiekt klasy Koło lub Kwadrat jest jednocześnie obiektem klasy Figura.

 

Dziedziczenie
Rysunek 1. Dziedziczenie

Mechanizm dziedziczenia

Dziedziczenie jest zależnością zupełnie innego rodzaju niż zwykłe związki pomiędzy klasami. Zwykłe związki (np. pomiędzy figurą a obrazem, do którego ta figura należy) opisują bowiem możliwe powiązania pomiędzy obiektami - egzemplarzami poszczególnych klas. Natomiast dziedziczenie występuje wyłącznie na poziomie klas - nie mówi nic o ewentualnych zależnościach pomiędzy obiektami klasy Koło a obiektami klasy Figura. Dlatego też przy strzałce reprezentującej dziedziczenie nie zapisujemy np. liczebności.

Nie znaczy to oczywiście, że dziedziczenie nie ma wpływu na obiekty. Powoduje ono bowiem, że wszystkie atrybuty i metody nadklasy oraz związki nadklasy z innymi klasami są dziedziczone do jej podklas. Zatem w klasie Koło nie musimy już definiować atrybutu nazwa, ponieważ jest on w niej obecny w wyniku dziedziczenia z klasy Figura. Znak # przy nazwie atrybutu w klasie Figura oznacza, że atrybut ten jest zastrzeżony (ang. protected), tzn. dostępny tylko w ramach danej klasy oraz jej podklas.

Podobnie, nie musimy tworzyć związku pomiędzy klasami Koło i ObrazWektorowy. Każde koło jest i tak przypisane do pewnego obrazu, ponieważ dziedziczy związek z klasą ObrazWektorowy z klasy Figura.

Na tej samej zasadzie podklasy dziedziczą metody zdefiniowane w nadklasach. W podklasie możemy jednak  przedefiniować metodę zdefiniowaną w nadklasie. Robimy tak zwykle wtedy, gdy implementacja metody dla obiektów podklasy musi być specyficzna ? inna niż dla obiektów nadklasy. W naszym przykładzie obie podklasy przedefiniowują metodę rysuj nadklasy. Jest to niezbędne, ponieważ każda z figur musi być rysowana w specyficzny dla siebie sposób.

Zwróćmy jeszcze uwagę, że nazwa klasy Figura jest napisana kursywą. Oznacza to, że Figura jest klasą abstrakcyjną  nie można tworzyć obiektów tej klasy. Możemy oczywiście tworzyć figury konkretnych rodzajów - będą to obiekty podklas klasy Figura. Jednak oznaczając klasę Figura jako abstrakcyjną, zabraniamy tworzenia figur nieokreślonego rodzaju.

Dziedziczenie jest mechanizmem bardzo eleganckim i wygodnym, trudno się więc dziwić, że jest chętnie wykorzystywane przez analityków, projektantów i programistów. Jednak nie w każdej sytuacji jest to mechanizm idealny. Przyjrzyjmy sie więc kilku przykładom, w których dziedziczenie nie jest najlepszym rozwiązaniem.

Modelowanie ról

Czytelnicy poprzednich odcinków naszego cyklu pamiętają zapewne, że dziedziczenie zwykle nienajlepiej nadaje się do modelowania ról pełnionych przez obiekty. Jako że jest to jeden z najczęściej spotykanych błędów analityczno-projektowych, przypomnijmy przykład modelu (rysunek 2), w którym kontrahent może pełnić rolę klienta lub dostawcy. Dziedziczenia nie powinniśmy używać wtedy, gdy nasz obiekt (kontrahent) może pełnić więcej niż jedną rolę w trakcie swojego życia. Wynika to z podstawowej zasady obiektowości: każdy obiekt jest obiektem tylko jednej klasy. Nie możemy więc utworzyć obiektu, który jest jednocześnie klasy Klient i klasy Dostawca (ani przeistoczyć obiektu klasy Klient w obiekt klasy Dostawca). W takich sytuacjach powinniśmy zrezygnować z dziedziczenia i zamodelować role jako relacje pomiędzy klasami, posiłkując się w razie potrzeby dodatkowymi klasami (więcej na ten temat w artykule ?Modelowanie ról na diagramach klas w języku UML?).

 

Niepoprawne wykorzystanie dziedziczenia do modelowania ról
Rysunek 2. Niepoprawne wykorzystanie dziedziczenia do modelowania ról

Niepotrzebne podklasy

Wyobraźmy sobie sklep internetowy, w którym mogą kupować zarówno klienci zarejestrowani (posiadający konto), jak i niezarejestrowani. Klient niezarejestrowany przy każdych zakupach podaje swoje dane adresowe, które musimy oczywiście przechowywać przez pewien czas w celu realizacji zamówienia. Klient zarejestrowany ma swoje konto i dzięki temu nie musi za każdym razem wpisywać od nowa wszystkich swoich danych. Klient zarejestrowany, którego sumaryczna wartość zakupów w ciągu roku przekroczy określony próg, otrzymuje status złotego klienta i zniżkę 10% na kolejne zakupy. Klienci niezarejestrowani nie otrzymują zniżek.

Jako że mamy tu do czynienia z kilkoma szczególnymi rodzajami klientów, moglibyśmy pokusić się o skonstruowanie modelu przedstawionego na rysunku 3. Model ten ma jednak jedną zasadniczą wadę. Klient zarejestrowany ? po dokonaniu odpowiednio dużych zakupów ? staje się złotym klientem. Tymczasem w naszym systemie został on utworzony jako obiekt klasy KlientZarejestrowany i nie może przekształcić się w obiekt klasy ZłotyKlient. Stosując taki model, musielibyśmy więc usunąć obiekt klasy KlientZarejestrowany i w jego miejsce utworzyć nowy obiekt klasy ZłotyKlient. Jest to rozwiązanie bardzo nieeleganckie.

Rozważając stworzenie podklas do pewnej klasy, powinniśmy przede wszystkim zastanowić się, czy te podklasy istotnie się od siebie różnią swoją zawartością informacyjną (atrybutami i związkami z innymi klasami) oraz swoim zachowaniem (metodami). Wtedy często okazuje się, że pewne niewielkie różnice pomiędzy podklasami można zapisać po prostu w atrybutach nadklasy. W naszym przykładzie klient zarejestrowany różni się od niezarejestrowanego wyłącznie faktem posiadania trwałego konta w systemie. Złoty klient ma zaś dodatkową metodę pozwalającą na naliczenie zniżki. Jednak nic nie stoi na przeszkodzie, aby taką metodę mieli wszyscy klienci. Metoda ta dla klientów nieuprawnionych do zniżki będzie po prostu naliczać zniżkę 0%.

Rozważania te prowadzą nas do modelu przedstawionego na rysunku 4. Zgodnie z nim, rozróżnienie pomiędzy klientem niezarejestrowanym a zarejestrowanym następuje na podstawie atrybutu zarejestrowany, zaś status złotego klienta jest oznaczany przypisaniem do klienta obiektu klasy PoziomKlienta. Zwróćmy uwagę, że w takim modelu można bez przebudowywania systemu oddawać kolejne poziomy lojalności klienta (np. platynowego klienta ze zniżką 20%) ? wystarczy w tym celu utworzyć nowy obiekt klasy PoziomKlienta, podając w jego atrybutach próg wartości zakupów oraz procent zniżki. Aby tego dokonać w modelu przedstawionym na rysunku 3., trzeba by dodać kolejną podklasę klasy Klient, co wiąże się oczywiście z przebudową systemu.

W następnym odcinku

Za miesiąc ciąg dalszy rozważań poświęconych dziedziczeniu. Zobaczymy, w jakich sytuacjach warto tworzyć wspólną nadklasę dla kilku klas oraz jak unikać nadmiernej hierarchii dziedziczenia.

Szymon Zioło

Artykuł został opublikowany w Software Developer's Journal nr 7/2009. 

Dodaj komentarz