Cześć! W dzisiejszym wpisie kontynuujemy kurs programowania obiektowego. W poprzednich postach poznaliście jego podstawy, a także 3 najważniejsze filary. Dzisiaj omówimy ostatni z nich: abstrakcję. Jeżeli przegapiliście poprzednie wpisy z serii, koniecznie nadróbcie zaległości:
Czym jest abstrakcja?
Abstrakcja polega na ukrywaniu wszelkich pól i metod tak, aby jak najmniejsza ich ilość była widoczna dla posiadających instancje tej klasy. Mówiąc prościej: korzystającego z naszej klasy powinien interesować tylko wynik wywołania konkretnej metody – reszta szczegółów jest dla niego nieistotna. Dzięki abstrakcji możemy zachować przejrzystość kodu, jednocześnie umożliwiając rozbudowę klasy o dodatkowe elementy, bez konieczności zmian wielu innych właściwości klasy.
Aby lepiej zobrazować czym jest abstrakcja weźmy prosty przykład z życia. Większość z nas ma wiele różnych urządzeń cyfrowych: smartfon, komputer, laptop itp. W przypadku gdy jedno z tych urządzeń się popsuje, szukamy odpowiedniego serwisu, który naprawi nasze urządzenie. Z racji, iż nie jesteśmy specami w sprawach sprzętowych, to nie interesuje nas w jaki sposób zostanie to zrobione, ale oczekujemy, że serwis odda naprawiony, w pełni sprawny sprzęt. Mało tego: każdy z serwisów może mieć różne sposoby naprawy, jednak od każdego z nich oczekujemy tego samego rezultatu. Na tym właśnie polega abstrakcja – znamy funkcje danej klasy, parametry jakie przyjmuje i typ jaki zwraca, ale w ogóle nie obchodzi nas to, co dzieje się wewnątrz metody.
Warstwy abstrakcji
Abstrakcję możemy podzielić na nieskończenie wiele warstw. Weźmy na przykład serwis internetowy – w naszej warstwie abstrakcji jest to witryna internetowa, która po wpisaniu danych daje odpowiednie wyniki – nie wiemy co się dzieję w warstwach niżej. Warstwą serwisu internetowego jest cała implementacja backendu i frontendu. Warstwą niższą dla serwisu internetowego jest kod w określonym języku programowania. Idąc dalej doszlibyśmy w końcu do warstwy języka maszynowego, prądu elektrycznego, warstwy atomowej itd. Obiekt z danej warstwy korzystając z metod z niższej warstwy abstrakcji nie ma pojęcia ile działań, ale też ile warstw kryje się pod danym wywołaniem – interesuje go tylko wynik.
Klasy abstrakcyjne i metody wirtualne
Klasy abstrakcyjne i metody wirtualne omawialiśmy już w poprzednim wpisie. Przypomnijmy sobie jednak najważniejsze zagadnienia:
- Klasy abstrakcyjne to klasy, których instancji nie da się stworzyć. Są to klasy, które definiują określone zachowania i właściwości obiektu, jednak nie posiadają ich implementacji. Klasy takie narzucają te implementacje klasom pochodnym, dzięki czemu możemy zdefiniować zachowanie wspólne dla wielu różnych klas. Klasę możemy uznać za abstrakcyjną, jeżeli posiada co najmniej jedną metodę czysto wirtualną.
- Metody czysto wirtualne to metody, które posiadają jedynie nagłówek, bez implementacji. To właśnie dlatego nie da się stworzyć instancji klas abstrakcyjnych – są one niekompletne. Metody czysto wirtualne muszą być nadpisane w klasach pochodnych, w przeciwnym wypadku klasa pochodna również będzie klasą abstrakcyjną.
- Metoda wirtualna to metoda, której implementacja znajduje się w klasie bazowej. Jej nadpisanie jest możliwe, ale nie jest w tym przypadku wymagane. Wywołanie metody wirtualnej skutkuje wywołaniem ostatniego nadpisania tej metody. Jeżeli ta metoda nie została nadpisana w drzewie dziedziczenia, to zostanie wywołana implementacja z klasy bazowej.
Znając podstawy abstrakcji możemy przejść do prostego przykładu:
Abstrakcja – przykład C++
Stwórzmy prostą, bazową klasę żołnierza. Żołnierz ten będzie posiadać imię, oraz wiek. Każdy z żołnierzy musisz również co jakiś czas zdawać status przełożonemu, więc stworzymy metodę czysto wirtualną. Tak prezentuje się klasa żołnierza:
Soldier.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#ifndef SOLDIER_H #define SOLDIER_H #include <string> class Soldier { public: Soldier(std::string name, unsigned age); //kontruktor naszej klasy virtual std::string getName(); //metoda wirtualna zwracajaca imie danego zolnieza virtual unsigned getAge(); //metoda wirtualna zwracajaca wiek zolnieza virtual std::string reportStatus() = 0; //metoda czysto wirtualna, ktorej zadaniem jest zgloszenie meldunku - od teraz nasza klasa jest abstrakcyjna private: std::string m_name; unsigned m_age; }; #endif // SOLDIER_H |
Soldier.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#include "soldier.h" Soldier::Soldier(std::string name, unsigned age) { m_name = name; //inicjalizujemy zmienne prywatne m_age = age; } std::string Soldier::getName() { return m_name; } unsigned Soldier::getAge() { return m_age; } |
Mając bazową klasę, spróbujmy stworzyć jej instancję w głównym pliku:
Main.cpp
1 2 3 4 5 6 7 |
#include <soldier.h> int main() { Soldier soldier("Zolniez1", 20); return 0; } |
Niestety, ale taki kod się nie skompiluje – jak już mówiliśmy, nie można stworzyć instancji obiektu klasy abstrakcyjnej. Musimy więc skorzystać z klasy pochodnej. Stworzymy w tym celu prostą klasę snajpera, która zaimplementuje metodę czysto wirtualną, a także nadpisze jedną z metod wirtualnych. Praca snajpera jest bardzo stresująca, dlatego przyjmijmy, że snajperzy wyglądają o 10 lat starzej, niż faktycznie mają. Wywołamy w tym celu metodę z klasy bazowej, a następnie zmodyfikujemy lekko jej wynik:
Sniper.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#ifndef SNIPER_H #define SNIPER_H #include "soldier.h" class Sniper : public Soldier { public: Sniper(std::string name, unsigned age); unsigned getAge() override; //nadpisujemy nasze metody wirtualne std::string reportStatus() override; //nadpisanie metody czysto wirtualnej jest wymagane, jezeli nie chcemy aby klasa byla abstrakcyjna }; #endif // SNIPER_H |
Sniper.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include "sniper.h" Sniper::Sniper(std::string name, unsigned age) : Soldier(name, age) //wywolujemy kontruktor klasy bazowej { } unsigned Sniper::getAge() { return Soldier::getAge() + 10; //z racji iz praca snajpera jest bardzo stresujaca, to przyjmujemy ze snajperzy wygladaja o 10 lat starzej niz maja lat } std::string Sniper::reportStatus() //nadpisujemy metode czysto wirtualna - kazdy zolniez moze zwrocic inny status { return "Byles w wojsku, Polaku?"; } |
Mamy pochodną naszej klasy abstrakcyjnej – możemy teraz zobaczyć jak sprawuje się w akcji:
1 2 3 4 5 6 7 8 9 10 |
#include <sniper.h> #include <iostream> int main() { // Soldier soldier("Zolniez1", 20); <---- taki kod nie zadziala, gdy probujemy utworzy instancje klasy abstrakcyjnej Sniper sniper ("Snajper1", 21); std::cout << sniper.getName() << " " << sniper.getAge() << " status: " << sniper.reportStatus(); return 0; } |
Mała uwaga: abstrakcja jest ściśle powiązana z polimorfizmem. Z tego też powodu, pomimo że nie można stworzyć instancji klasy abstrakcyjnej, to możliwe jest stworzenie wskaźnika lub referencji na daną klasę – pod taką zmienną może ukrywać się instancja dowolnej klasy pochodnej. Dzięki takiemu mechanizmowi możemy wywoływać metody z klasy, nie znając do końca jej konkretnego typu:
1 2 3 4 5 6 7 8 9 10 11 12 |
#include <sniper.h> #include <iostream> int main() { // Soldier soldier("Zolniez1", 20); <---- taki kod nie zadziala, gdy probujemy utworzy instancje klasy abstrakcyjnej Sniper sniper ("Snajper1", 21); Soldier *soldier = &sniper; //mozna rowniez skorzystac z referencji na klase abstrakcyjna (Soldier &soldier = sniper;) std::cout << soldier->getName() << " " << soldier->getAge() << " status: " << soldier->reportStatus(); return 0; } |
Podsumowanie
W dzisiejszej lekcji dowiedzieliście się czym jest abstrakcja. Jako, iż omówiliśmy dzisiaj ostatni z filarów programowania obiektowego, to od następnej lekcji zanurzymy się nieco głębiej w tajniki obiektowości: dowiecie się czym dokładnie są konstruktory i destruktory. Jeżeli chesz być na bieżąco, zapisz się do naszej listy mailingowej klikając w kopertę z menu po prawej.