1.7.pdf

(751 KB) Pobierz
Od zera do gier kodera
7
PROGRAMOWANIE
OBIEKTOWE
Gdyby murarze budowali domy tak,
jak programiści piszą programy,
to jeden dzięcioł zniszczyłby całą cywilizację.
ze zbioru prawd o oprogramowaniu
Witam cię serdecznie, drogi Czytelniku! Powitanie to jest tutaj jak najbardziej wskazane.
Twoja obecność wskazuje bowiem, że nadzwyczaj szybko wydostałeś się spod sterty
nowych wiadomości, którymi obarczyłem cię w poprzednim rozdziale :) A nie było to
wcale takie proste, zważywszy że poznałeś tam zupełnie nową technikę programowania,
opierającą się na całkiem innych zasadach niż te dotychczas ci znane.
Mimo to mogłeś uczuć pewien niedosyt. Owszem, idea OOPu była tam przedstawiona
jako w miarę naturalna, a nawet intuicyjna (w każdym razie bardziej niż programowanie
strukturalne). Potrzeba jednak sporej dozy optymizmu, aby uznać ją na tym etapie za
coś rewolucyjnego, co faktycznie zmienia sposób myślenia o programowaniu (a
jednocześnie znacznie je ułatwia).
By w pełni przekonać się do tej koncepcji, trzeba o niej wiedzieć nieco więcej; kluczowe
informacje na ten temat są zawarte w tym oto rozdziale. Sądzę więc, że choćby z tego
powodu będzie on dla ciebie bardzo interesujący :D
Zajmiemy się w nim dwoma niezwykle ważnymi zagadnieniami programowania
obiektowego: dziedziczeniem oraz metodami wirtualnymi. Na nich właśnie opiera się cała
jego potęga, pozwalająca tworzyć efektowne i efektywne programy.
Zobaczymy zresztą, jak owo tworzenie wygląda w rzeczywistości. Końcową część
rozdziału poświęciłem bowiem na zestaw rad i wskazówek, które, jak sądzę, okażą się
pomocne w projektowaniu aplikacji opartych na modelu OOP.
Kontynuujmy zatem poznawanie wspaniałego świata programowania obiektowego :)
Dziedziczenie
Drugim powodem, dla którego techniki obiektowe zyskały taką popularność 77 , jest
znaczący postęp w kwestii ponownego wykorzystywania raz napisanego kodu oraz
rozszerzania i dostosywania go do własnych potrzeb.
Cecha ta leży u samych podstaw OOPu: program konstruowany jako zbiór
współdziałających obiektów nie jest już bowiem monolitem, ścisłym połączeniem danych i
wykonywanych nań operacji. „Rozdrobniona” struktura zapewnia mu zatem
modularność : nie jest trudno dodać do gotowej aplikacji nową funkcję czy też
77 Pierwszym jest wspominana nie raz „naturalność” programowania, bez konieczności podziału na dane i kod.
76934931.020.png
214
wyodrębnić z niej jeden podsystem i użyć go w kolejnej produkcji. Ułatwia to i
przyspiesza realizację kolejnych projektów.
Wszystko zależy jednak od umiejętności i doświadczenia programisty. Nawet stosując
techniki obiektowe można stworzyć program, którego elementy będą ze sobą tak ściśle
zespolone, że próba ich użycia w następnej aplikacji będzie przypominała wciskanie
słonia do szklanej butelki.
Istnieje jeszcze jedna przyczyna, dla której kod oparty na programowaniu obiektowym
łatwiej poddaje się „recyklingowi”, mającemu przygotować go do ponownego użycia. Jest
nim właśnie tytułowy mechanizm dziedziczenia.
Korzyści płynące z jego stosowania nie ograniczają się jednakże tylko do wtórnego
„przerobu” już istniejącego kodu. Przeciwnie, jest to fundamentalny aspekt OOPu
niezmiernie ułatwiający i uprzyjemniający projektowanie każdej w zasadzie aplikacji. W
połączeniu z technologią funkcji wirtualnych oraz polimorfizmu daje on niezwykle
szerokie możliwości, o których szczegółowo traktuje praktycznie cały niniejszy rozdział.
Rozpoczniemy zatem od dokładnego opisu tego bardzo pożytecznego mechanizmu
programistycznego.
O powstawaniu klas drogą doboru naturalnego
Człowiek jest taką dziwną istotą, która bardzo lubi posiadać uporządkowany i
usystematyzowany obraz świata. Wprowadzanie porządku i pewnej hierarchii co do
postrzeganych zjawisk i przedmiotów jest dla nas niemal naturalną potrzebą.
Chyba najlepiej przejawia się to w klasyfikacji biologicznej. Widząc na przykład psa
wiemy przecież, że nie tylko należy on do gatunku zwanego psem domowym, lecz także
do gromady znanej jako ssaki (wraz z końmi, słoniami, lwami, małpami, ludźmi i całą
resztą tej menażerii). Te z kolei, razem z gadami, ptakami czy rybami należą do kolejnej,
znacznie większej grupy organizmów zwanych po prostu zwierzętami.
Nasz pies jest zatem jednocześnie psem domowym, ssakiem i zwierzęciem:
Schemat 22. Klasyfikacja zwierząt jako przykład hierarchii typów obiektów
76934931.021.png 76934931.022.png 76934931.023.png 76934931.001.png 76934931.002.png 76934931.003.png 76934931.004.png 76934931.005.png 76934931.006.png
215
Gdyby był obiektem w programie, wtedy musiałby należeć aż do trzech klas naraz 78 !
Byłoby to oczywiście niemożliwe, jeżeli wszystkie miałyby być wobec siebie równorzędne.
Tutaj jednak tak nie jest: występuje między nimi hierarchia, jedna klasa pochodzi od
drugiej. Zjawisko to nazywamy właśnie dziedziczeniem .
Dziedziczenie (ang. inheritance ) to tworzenie nowej klasy na podstawie jednej lub kilku
istniejących wcześniej klas bazowych.
Wszystkie klasy, które powstają w ten sposób (nazywamy je pochodnymi ), posiadają
pewne elementy wspólne. Części te są dziedziczone z klas bazowych, gdyż tam właśnie
zostały zdefiniowane.
Ich zbiór może jednak zostać poszerzony o pola i metody specyficzne dla klas
pochodnych. Będą one wtedy współistnieć z „dorobkiem” pochodzącym od klas
bazowych, ale mogą oferować dodatkową funkcjonalność.
Tak w teorii wygląda system dziedziczenia w programowaniu obiektowym. Najlepiej
będzie, jeżeli teraz przyjrzymy się, jak w praktyce może wyglądać jego zastosowanie.
Od prostoty do komplikacji, czyli ewolucja
Powróćmy więc do naszego przykładu ze zwierzętami. Chcąc stworzyć programowy
odpowiednik zaproponowanej hierarchii, musielibyśmy zdefiniować najpierw odpowiednie
klasy bazowe . Następnie odziedziczylibyśmy ich pola i metody w klasach
pochodnych i dodali nowe, właściwe tylko im. Powstałe klasy same mogłyby być potem
bazami dla kolejnych, jeszcze bardziej wyspecjalizowanych typów.
Idąc dalej tą drogą dotarlibyśmy wreszcie do takich klas, z których sensowne byłoby już
tworzenie normalnych obiektów.
Pojęcie klas bazowych i klas pochodnych jest zatem względne : dana klasa może
wprawdzie pochodzić od innych, ale jednocześnie być bazą dla kolejnych klas. W ten
sposób ustala się wielopoziomowa hierarchia, podobna zwykle do drzewka.
Ilustracją tego procesu może być poniższy diagram:
Schemat 23. Hierarchia klas zwierząt
78 A raczej do siedmiu lub ośmiu, gdyż dla prostoty pominąłem tu większość poziomów systematyki.
76934931.007.png 76934931.008.png 76934931.009.png 76934931.010.png 76934931.011.png 76934931.012.png 76934931.013.png 76934931.014.png 76934931.015.png
216
Wszystkie przedstawione na nim klasy wywodzą się z jednej, nadrzędnej wobec
wszystkich: jest nią naturalnie klasa Zwierzę . Dziedziczy z niej każda z pozostałych klas -
bezpośrednio , jak Ryba , Ssak oraz Ptak , lub pośrednio - jak Pies domowy .
Tak oto tworzy się kilkupoziomowa klasyfikacja oparta na mechanizmie dziedziczenia.
Z klasy bazowej do pochodnej, czyli dziedzictwo przodków
O podstawowej konsekwencji takiego rozwiązania zdążyłem już wcześniej wspomnieć.
Jest nią mianowicie przekazywanie pól oraz metod pochodzących z klasy bazowej do
wszystkich klas pochodnych, które się z niej wywodzą. Zatem:
Klasa pochodna zawiera pola i metody odziedziczone po klasach bazowych. Może także
posiadać dodatkowe, unikalne dla siebie składowe - nie jest to jednak obowiązkiem.
Prześledźmy teraz sposób, w jaki odbywa się odziedziczanie składowych na przykładzie
naszej prostej hierarchii klas zwierząt.
U jej podstawy leży „najbardziej bazowa” klasa Zwierzę . Zawiera ona dwa pola,
określające masę i wiek zwierzęcia, oraz metody odpowiadające za takie czynności jak
widzenie i oddychanie. Składowe te mogły zostać umieszczone tutaj, gdyż dotyczą one
wszystkich interesujących nas zwierząt i będą miały sens w każdej z klas pochodnych.
Tymi klasami, bezpośrednio dziedziczącymi od klasy Zwierzę , są Ryba , Ssak oraz Ptak .
Każda z nich niejako „z miejsca” otrzymuje zestaw pól i metod, którymi legitymowało
się bazowe Zwierzę . Klasy te wprowadzają jednak także dodatkowe, własne metody: i
tak Ryba może pływać, Ssak biegać 79 , zaś Ptak latać. Nie ma w tym nic dziwnego,
nieprawdaż? :)
Wreszcie, z klasy Ssak dziedziczy najbardziej interesująca nas klasa, czyli Pies domowy .
Przejmuje ona wszystkie pola i metody z klasy Ssak , a więc pośrednio także z klasy
Zwierzę . Uzupełnia je przy tym o kolejne składowe, właściwe tylko sobie.
Ostatecznie więc klasa Pies domowy zawiera znacznie więcej pól i metod niż mogłoby się
z początku wydawać:
Schemat 24. Składowe klasy Pies domowy
79 Delfiny muszą mi wybaczyć nieuwzględnienie ich w tym przykładzie :D
76934931.016.png 76934931.017.png 76934931.018.png
217
Wykazuje poza tym pewną budowę wewnętrzną: niektóre jej pola i metody możemy
bowiem określić jako własne i unikalne, zaś inne są odziedziczone po klasie bazowej i
mogą być wspólne dla wielu klas. Nie sprawia to jednak żadnej różnicy w korzystaniu z
nich: funkcjonują one identycznie, jakby były zawarte bezpośrednio wewnątrz klasy.
Obiekt o kilku klasach, czyli zmienność gatunkowa
Oczywiście klas nie definiuje się dla samej przyjemności ich definiowania, lecz dla
tworzenia z nich obiektów. Jeżeli więc posiadalibyśmy przedstawioną wyżej hierarchię w
jakimś prawdziwym programie, to z pewnością pojawiłyby się w nim także instancje
zaprezentowanych klas, czyli odpowiednie obiekty.
W ten sposób wracamy do problemu postawionego na samym początku: jak obiekt może
należeć do kilku klas naraz? Różnica polega wszak na tym, że mamy już jego gotowe
rozwiązanie :) Otóż nasz obiekt psa należałby przede wszystkim do klasy Pies
domowy ; to właśnie tej nazwy użylibyśmy, by zadeklarować reprezentującą go zmienną
czy też pokazujący nań wskaźnik. Jednocześnie jednak byłby on typu Ssak oraz typu
Zwierzę , i mógłby występować w tych miejscach programu, w których byłby wymagany
jeden z owych typów.
Fakt ten jest przyczyną istnienia w programowaniu obiektowym zjawiska zwanego
polimorfizmem. Poznamy je dokładnie jeszcze w tym rozdziale.
Dziedziczenie w C++
Pozyskawszy ogólne informacje o dziedziczeniu jako takim, możemy zobaczyć, jak idea
ta została przełożona na nasz nieoceniony język C++ :) Dowiemy się więc, w jaki sposób
definiujemy nowe klasy w oparciu o już istniejące oraz jakie dodatkowe efekty są z tym
związane.
Podstawy
Mechanizm dziedziczenia jest w C++ bardzo rozbudowany, o wiele bardziej niż w
większości pozostalych języków zorientowanych obiektowo 80 . Udostępnia on kilka
szczególnych możliwości, które być może nie są zawsze niezbędne, ale pozwalają na dużą
swobodę w definiowaniu hierarchii klas. Poznanie ich wszystkich nie jest konieczne, aby
sprawnie korzystać z dobrodziejstw programowania obiektowego, jednak wiemy
doskonale, że wiedza jeszcze nikomu nie zaszkodziła :D
Zaczniemy oczywiście od najbardziej elementarnych zasad dziedziczenia klas oraz
przyjrzymy się przykładom ilustrującym ich wykorzystanie.
Definicja klasy bazowej i specyfikator protected
Jak pamiętamy, definicja klasy składa się przede wszystkim z listy deklaracji jej pól oraz
metod, podzielonych na kilka części wedle specyfikatorów praw dostępu. Najczęściej
każdy z tych specyfikatorów występuje co najwyżej w jednym egzemplarzu, przez co
składnia definicji klasy wygląda następująco:
class nazwa_klasy
{
[ private : ]
[ deklaracje_prywatne ]
[ protected : ]
[ deklaracje_chronione ]
[ public : ]
80 Dorównują mu chyba tylko rozwiązania znane z Javy.
76934931.019.png
 
Zgłoś jeśli naruszono regulamin