Programowanie obiektowe #1 – co to jest dziedziczenie?

Witajcie w kolejnym wpisie z serii o programowaniu obiektowym! Poprzednio dowiedzieliście się czym jest paradygmat programowania obiektowego, a także poznaliście podstawowe filary obiektowości. Dzisiaj omówimy bliżej jeden z nich: dziedziczenie. Dziedziczenie to najbardziej charakterystyczny paradygmat, z którego słyną języki obiektowe, dlatego ważne jest jego dobre zrozumienie.

Czym jest dziedziczenie?  

Zacznijmy od krótkiego przypomnienia. Dziedziczenie jest mechanizmem, który pozwala na współdzielenie własności i zachowań pomiędzy klasami. Mówiąc ściślej, dzięki temu mechanizmowi możemy tworzyć bardziej wyspecjalizowane klasy na podstawie klas bazowych. Oszczędzamy w ten sposób czas i pamięć, ponieważ nie musimy tworzyć kilkukrotnie klas posiadających te same własności.

Żeby lepiej zrozumieć dziedziczenie, przejdźmy do przykładu. Powiedzmy, że chcemy stworzyć reprezentację firmy produkującej pojazdy: firma ta produkuje auta, motocykle, autobusy i traktory. Na pierwszy rzut oka wydawać by się mogło, że wystarczy stworzyć klasę dla każdego z tych pojazdów. Łatwo jednak zauważyć, że każdy z nich posiada pewne wspólne cechy, jak np. pojemność silnika, ilość koni mechanicznych, typ paliwa, czy kolor – są to ich współdzielone właściwości. Ponadto każdy z nich może jechać w przód, w tył, a także hamować – są to ich wspólne zachowania.

W takiej sytuacji wystarczy stworzyć klasę Vehicle, następnie na jej podstawie utworzyć klasy Car, Motorbike, Bus oraz Tractor. Dzięki temu każdy z tych obiektów będzie miał cechy charakterystyczne dla pojazdów mechanicznych, określone w klasie Vehicle. Dodatkowo każda z tych klas będzie mogła implementować charakterystyczne dla siebie pola i metody.

Dziedziczenie konstruktorów  

Gdy tworzymy klasy pochodne, to dziedziczą one wszystkie widoczne cechy klasy bazowej (o widoczności opowiemy więcej za chwilę). Do tych cech możemy również wliczyć konstruktory, oraz destruktory. Konstruktor jest specjalną metodą, która jest wywoływana w trakcie tworzenia instancji klasy. Często przyjmuje ona kilka parametrów, wymaganych do inicjalizacji zmiennych. Destruktor zaś jest metodą, którą wywołuje się w momencie usuwania instancji klasy – tutaj powinniśmy po sobie „posprzątać”, czyli zwolnić wszelką pamięć zajmowaną przez elementy klasy.

Dzięki dziedziczeniu konstruktorów nie musimy wielokrotnie powtarzać tych samych czynności w  konstruktorach klas pochodnych. Warto tu jednak zaznaczyć, że przy tworzeniu instancji klasy pochodnej najpierw wywoływany jest konstruktor klasy bazowej. Konstruktor klasy pochodnej, zanim zacznie jakiekolwiek inne działanie, odwołuje się bezpośrednio do konstruktora klasy bazowej.

Widoczność pól i metod  

Tworząc klasę bazową trzeba pamiętać o zakresie widoczności pól i metod. Metody i pola private są widoczne tylko i wyłącznie w obrębie danej klasy – żadna z innych klas, nawet dziedziczących, nie ma do nich dostępu. Słowo kluczowe public ujawnia je każdemu, kto ma dostęp do instancji danych klas. Co w sytuacji, w której nie chcemy ujawniać pola/metody, ale chcemy, żeby były one dostępne dla klas dziedziczących? Wystarczy użyć zakresu protected – wszystko oznaczone tym słówkiem będzie widoczne tylko i wyłącznie w obrębie danej klasy i jej pochodnych.

Zakres dziedziczenia

W języku C++ możliwe jest również określenie zakresu dziedziczenia po klasie bazowej. Zakres ten możemy ustalić przy użyciu modyfikatora tuż przed nazwą dziedziczonej klasy. Podobnie jak w przypadku metod i pól klasy rozróżniamy trzy zakresy:

  • public – zakres dostępności metod i pól nie ulega zmianie po odziedziczeniu
  • protected – metody i pola z zakresu public i protected po odziedziczeniu zyskują zakres protected;
  • private – wszelkie publiczne i chronione metody/pola po odziedziczeniu będą dostępne jedynie w zakresie private

Należy pamiętać, że w każdym z zakresów dziedziczenia prywatne metody/pola nie są widoczne dla klasy pochodnej.

Nadpisywanie metod

Dziedzicząc klasę mamy możliwość nadpisać niektóre z jej metod, dzięki czemu możemy zmienić jej zachowanie, jeżeli nasza pochodna klasa tego wymaga. Gdy metoda nie została w żaden sposób nadpisana, to wywołując ją np. w klasie Car tak naprawdę wywołamy ją w klasie Vehicle, która została odziedziczona przez klasę samochodu. Trzeba jednak pamiętać, że jeśli wykonamy rzutowanie do bazowej klasy instancji klasy samochodu, to zostanie wywołana bazowa metoda, nawet jeżeli została ona nadpisana w pochodnej klasie. Rzutowanie jest operacją, która pozwala interpretować obiekt jakby był instancją innej klasy – czasem dodatkowe metody/pola nie są potrzebne. Omawiane tutaj nadpisywanie jest również ściśle związane z polimorfizmem, o którym dowiecie się więcej w następnych wpisach.

Zapobieganie dziedziczeniu  

Czasem zdarza się, że nie chcemy, aby dana klasa była jakkolwiek dziedziczona. W takiej sytuacji należy dodać odpowiedni modyfikator przy deklaracji klasy. Modyfikator ten ma różną postać – jest ona zależna od danego języka programowania. W przypadku C++, na którym opieramy ten kurs, wystarczy dodać słówko final po nazwie klasy. W karierze IT możesz się spotkać również z frazą sealed. Niezależnie jednak od postaci modyfikatora, ma on ten sam rezultat: nie można utworzyć klas pochodnych na podstawie takiej klasy – kompilator wypluje wtedy błąd.

Przykład  

Zastosujmy nowo nabytą wiedzę w praktyce. Poniżej znajdziecie deklarację bazowej klasy pojazdu: Vehicle.

Vehicle.h

Vehicle.cpp

Jak widzicie stworzyliśmy klasę Vehicle. Jak na razie posiada ona jedynie bazowe metody i pola. Konstruktor pozwala na szybkie nadanie wartości polom klasy według podanych parametrów. Zwróć uwagę, że pola klasy stworzyliśmy w sekcji private, a ich wartości zwracamy w publicznych metodach. Jest to często stosowana praktyka, mająca na celu ochronę danych przed niekontrolowaną modyfikacją. Jest to element hermetyzacji, ale o dowiesz się o niej więcej w innym wpisie. Zauważ też, że pola prywatne zaczynają się znakiem _ – ma to na celu zwiększenie przejrzystości kodu i zmniejszenie ryzyka pomyłki.

Mamy już więc bazową klasę. Pora więc stworzyć klasy pochodne:

Motorbike.h

Motorbike.cpp

Stworzyliśmy klasę motocykla. Zauważ, że nadpisaliśmy metodę trąbienia z klasy bazowej. Dodatkowo wywołujemy ją w konstruktorze klasy – wywoła się ona od razu po utworzeniu instancji obiektu.

Car.h

Car.cpp

Teraz stworzyliśmy klasę samochodu. Zawiera ona kilka, charakterystycznych dla siebie metod. Zauważ, że z poziomu klasy pochodnej nie mamy dostępu do prywatnego pola _name – musimy się do niego dostać poprzez publiczną metodę getName().

Tractor.h

Tractor.cpp

Kolejną pochodną jest klasa ciągnika. W klasie tej nadpisaliśmy funkcję hamowania: teraz ciągnik trąbi, zanim zahamuje.

Pora zebrać wszystko do kupy i sprawdzić działanie naszych klas w akcji:

Main.cpp

Zwróć uwagę, że możemy wywoływać metody charakterystyczne dla klasy Vehicle z każdej instancji jej klas pochodnych. Działanie jednak może się różnić, zależnie od tego jak określiliśmy je w konstruktorze i/lub nadpisanych metodach

A oto efekt uruchomienia powyższego programu:

Jak widzisz klasa Motorbike, zgodnie z zamiarami, w momencie stworzenia wywołała zmodyfikowaną metodę trąbienia. Klasa Tractor zaś wywołała metodę trąbienia podczas hamowania – dokładnie tak jak to planowaliśmy. Poniżej widzimy również efekty działania metod charakterystycznych dla każdej z klas.

Co ciekawe, klasy możemy dziedziczyć do momentu, aż któraś będzie oznaczona jako final. Oznacza to, że moglibyśmy stworzyć klasy pochodne od Car, np. klasy samochodów terenowych, wyścigowych, dostawczych itd. Dziedzicząc wiele klas trzeba jednak uważać, bo możemy doprowadzić do problemu diamentu. Występuje on w sytuacji, gdy dana klasa dziedziczy kilka różnych klas, spośród których co najmniej dwie mają wspólną klasę bazową. W takiej sytuacji ich własności i zachowania się pokrywają, przez co program nie wie z której dziedziczonej klasy ma korzystać, w przypadku np. wywołania wspólnej dla nich metody.

grupa wsparcia matura z informatyki

Podsumowanie  

W dzisiejszym wpisie dowiedzieliście się czym jest dziedziczenie, a także bliżej poznaliście się z jego zasadami. Wspólnie sprawdziliśmy na czym ono dokładnie polega przy użyciu przykładowych klas. W następnym wpisie dowiecie się dokładniej czym jest polimorfizm (którego elementy mogliście zauważyć już dzisiaj). Udanej środy 😀

You Might Also Like
Dodaj komentarz

icon