Witajcie! W dzisiejszym wpisie dowiecie się czym jest polimorfizm, a także jak on działa. Zanim jednak przejdziemy do wpisu chcemy życzyć wszystkim maturzystom połamania długopisów i klawiatur na dzisiejszej maturze z informatyki – mamy nadzieję, że materiały z naszego bloga przyczynią się ku waszemu sukcesowi
Polimorfizm – definicja
Polimorfizm, jak pewnie pamiętacie z pierwszej lekcji, jest jednym z czterech filarów, na których opiera się programowanie obiektowe – dziedziczenie i hermetyzację omówiliśmy już w poprzednich wpisach. W krócie polega on na definiowaniu innych zachowań tej samej funkcji dla różnych, często dziedziczących po tym samym rodzicu, klas. Polimorfizm jest więc silnie powiązany z dziedziczeniem. Wynika to z faktu, że najczęściej w klasie bazowej definiowane są metody wirtualne, których nadpisanie jest wymagane w klasach pochodnych. Istnieją także metody czysto wirtualne, które w klasie bazowej nie mają implementacji, co czyni zawierające je klasy abstrakcyjnymi.
Czym jest klasa abstrakcyjna? Jest to klasa, której instancji nie da się stworzyć. Oznacza to, że nie da się jej wykorzystać jako typu obiektu. W języku C++ możliwe jest jednak zwracanie referencji na klasę abstrakcyjną, co wykorzystamy w późniejszym przykładzie. Jeżeli metoda czysto wirtualna nie zostanie nadpisana w klasach pochodnych, to one z automatu też stają się klasami abstrakcyjnymi.
Każdy wielokąt możemy opisać tymi samymi właściwościami: ilością boków, obwodem, polem, kątami, przekątnymi, zachowaniem przekątnych etc. Każdy z katów ma jednak swoje własne właściwości i zachowania.
Polimorfizm – typy
Ze względu na sposób wdrożenia, polimorfizm możemy rozróżnić na dwa typy. Pierwszy z nich polega na przeciążaniu metod lub operatorów. Oznacza to, że tą samą metodę możemy zdefiniować wielokrotnie, jednak każda z tych definicji różni się ilością i/lub typami przyjmowanych argumentów. Często zdarza się też, że metody o mniej rozbudowanej konstrukcji korzystają z tych bardziej skomplikowanych, przekazując domyślne wartości w brakujących argumentach. Przykładowo klasa Boss może zawierać metodę paySalary(intamount, string message, Workerworker), która wypłaca amountpieniędzy pracownikowi worker, przekazując mu jednocześnie wiadomość o treści message. Szef tej firmy jest jednak bardzo gburowaty. Z tego powodu często przy wypłacie bez powodu krzyczy na pracownika, dlatego w jego klasie znajduje się również metoda paySalary(intamount, Workerworker), która wywołuje metodę o większej ilości argumentów, przekazując amount, wiadomość „Następnym razem Cię zwolnię!” i worker jako parametry.
Drugi typ polimorfizmu polega na definiowaniu tych samych metod w różny sposób w klasach pochodnych. Dzięki temu możemy określić specyficzne dla danego typu działanie funkcji, jednocześnie pozwalając na wywołanie jej bez dokładnej znajomości danego typu. Przykładowo chcemy wywołać metodę work() każdego z pracowników firmy. Wiemy, że każdy wykonuje pracę inaczej, nie znamy jednak konkretnych typów pracowników – wiemy jedynie, że wszystkie dziedziczą po klasie Worker. Okazuje się, że to już nam w zupełności wystarcza. Wystarczy bowiem do klasy Worker dodać metodę wirtualną, której implementacja jest wymagana w każdym z dziedziczących typów. Sama klasa bazowa nie może jednak implementować metody wirtualnej, dlatego przy wywołaniu metody work() każdego z pracowników wywoła się metoda specyficzna dla niego. Trochę zawikłane, prawda? Jednak, aby rozwiać wszelkie wątpliwości przejdźmy do przykładu.
Polimorfizm – przykład
Dla przykładu stworzymy klasę komórki. W głównej metodzie programu stworzymy obszerny zbiór komórek, a następnie co określony czas będziemy wywoływać metody „tik” każdej z nich. Załóżmy, że komórka będzie przechowywała pewną ilość energii, którą będzie zużywała podczas każdego tiku życia. Określimy również ilość cykli, którą dana komórka będzie żyła. Warto również dodać metodę, która będzie sprawdzać czy dana komórka nadal żyje – przyjmijmy, że komórka jest żywa dopóki nie przekroczyła maksymalnej długości życia, a także posiada jeszcze jakąś energię. Dodatkowo dodamy dwie metody wirtualne: jedna będzie odpowiadała za wytwarzanie energii, a druga za rozmnażanie komórki. Tak wygląda implementacja klasy komórki:
Cell.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#ifndef CELL_H
#define CELL_H
#include <string>
#include <iostream>
using namespacestd;
classCell
{
public:
Cell(conststring&cellName,intinitialEnergy,intmaxAge);//konstruktor klasy komorki
voidlifeTick();//pojedynczy tick zycia komorki
boolisAlive();//metoda zwracajaca, czy dana komorka jest nadal zywa
std::stringgetName();//tworzymy publiczne metody ujawniajace informacje o komorce
intgetEnergy();
intgetMaxAge();
intgetAge();
//teraz definiujemy metody czysto wirtualne, ktorych odziedziczenie, a takze deklaracja jest wymagana w klasach pochodnych
virtual voidgainEnergy()=0;//metoda czysto wirtualna, ktora pozyskuje energie dla danej komorki
virtual Cell*reproduce()=0;//metoda czysto wirtualna podzialu komorki - zwraca nowa komorke
protected:
//tworzymy metody gettery dla prywatnych pol, aby byly dostepne dla klas dziedziczacych
intgetChildCount();
voidsetEnergy(intamount);//a takze metode pozwalajca modyfikowac ilosc aktualnej energii
voidsetChildCount(intamount);//i zmieniajaca ilosc dzieci
private:
int_energy=1000;//energia pojedynczej komorki
int_age=0;//wiek komorki
int_maxAge=20;//maksymalny wiek do jakiego dozyje komorka
std::string_name;//nazwa komorki
int_childCount=0;//zmienna przechowujaca ilosc dzieci komorki
_name=cellName;//nadajemy odpowiednie wartosci zmiennym w konstruktorze
_energy=initialEnergy;
_maxAge=maxAge;
}
voidCell::lifeTick()
{
_energy-=100+_energy *0.3;//kazda komorka co kazdy tik zuzywa taka sama ilosc energii
_age++;//a takze zwieksza swoj wiek o jeden
}
boolCell::isAlive()
{
return_age<_maxAge&&_energy>0;//komorke uznajemy za zywa, gdy posiada dostatecznie duzo energii, a takze gdy nie przekroczyla maksymalnego limitu zycia
}
intCell::getEnergy()
{
return_energy;
}
intCell::getMaxAge()
{
return_maxAge;
}
intCell::getAge()
{
return_age;
}
std::stringCell::getName()
{
return_name;
}
voidCell::setEnergy(intamount)
{
_energy=amount;
}
voidCell::setChildCount(intamount)
{
_childCount=amount;
}
intCell::getChildCount()
{
return_childCount;
}
Następnie utwórzmy pochodną klasę komórki roślinnej. Jak już wiecie musi ona implementować z góry narzucone metody wirtualne klasy bazowej. Musimy więc określić w jakich warunkach i w jaki sposób komórka się reprodukuje, a także w jaki sposób pozyskuje energie. Stworzymy w tym celu uproszczoną metodę fotosyntezy, która będzie generowała losową ilość energii. Założymy też, że komórka zreprodukuje się tylko, gdy będzie miała odpowiednią ilość energii. Jeżeli tak, to podzieli ją na równo pomiędzy nowoutworzoną komórką a sobą. Poniżej kod klasy:
PlantCell.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#ifndef PLANTCELL_H
#define PLANTCELL_H
#include <Cell.h>
classPlantCell:publicCell
{
public:
PlantCell(conststd::string&id,intinitialEnergy);//konsturktor klasy roslinnej
Cell*reproduce()override;//deklaracja nadpisan wirtualnych metod dziedziczonych od rodzica
voidgainEnergy()override;
protected:
voidbeginPhotosynthesis();//metoda rozpoczynajaca produkcje energii - nie chcemy, aby byla dostepna na zewnatrz klasy
if(getEnergy()>400&&getAge()>3){//do reprodukcji dojdzie tylko wtedy, gdy komorka jest zywa i posiada dostatecznie duzo energii
intfinalEnergy=getEnergy()/2;//nowo utworzona komorka bedzie posiadala polowe posiadanej przez rodzica energii
setEnergy(finalEnergy);//pomniejszamy energie komorki o polowe
setChildCount(getChildCount()+1);//zwiekszamy ilosc dzieci o jeden
returnnewPlantCell(getName()+":"+to_string(getChildCount()),finalEnergy);//zwracamy wskaznik na nowa komorke
}
else{
returnnullptr;//w przeciwnym wypadku zwracamy pusty wskaznik - nie zostala stworzona zadna nowa komorka
}
}
voidPlantCell::gainEnergy()
{
beginPhotosynthesis();//komorka roslinna pozyskuje energie przez fotosynteze
}
voidPlantCell::beginPhotosynthesis()
{
//duuuzo reakcji z wieloma skladnikami
intenergy=getEnergy()+((rand()%600+70));
setEnergy(energy);//przyjmujemy ze ilosc energii byla zalezna od wieluu czynnikow
//skorzystamy wiec z bliblioteki rand, aby wygenerowala losowa ilosc energii od 40 do 400 jednostek
}
Następnie stworzymy klasę komórki zwierzęcej. Tutaj dodamy ilość przechowywanego pokarmu, a także stworzymy przeciążoną metodę pozyskiwania go. Następnie, tak jak w przypadku komórki roślinnej, musimy zaimplementować dziedziczone metody. Przyjmijmy, że klasa reprodukuje się, gdy ma wystarczająco pokarmu, a następnie dzieli się nim z nowo utworzoną komórką.
<<left<<setw(7)<<setfill(' ')<<to_string(c->getAge())+"/"+to_string(c->getMaxAge())<<endl;//drukujemy informacje o komorce
}
i++;
}
for(autoc:toRemove){//po iteracji wyrzucamy z glownego kontenera komorki martwe
Cell*cell=cells.at(c);
cells.erase(cells.begin()+c);
delete cell;//nalezy pamietac o zwalnianiu pamieci
}
for(autoc:toAdd){
cells.push_back(c);//a nastepnie dodajemy te nowe
}
toRemove.clear();
toAdd.clear();
_sleep(500);//petla bedzie wykonywac sie co pol sekundy
}
return0;
}
Zwróćcie uwagę, że w pętli nie usuwamy/dodajemy komórek do głównego kontenera. Dlaczego? Modyfikacja zbioru iterowanych danych zmieniłaby zakres działania pętli, przez co program najprawdopodobniej by się posypał. Skorzystaliśmy więc z pomocniczych kontenerów, których zawartość sukcesywnie obsługiwaliśmy poza pętlą. Zauważcie, że w pętli for nie wiemy jakiego typu są przechowywane komórki, a jednak jesteśmy w stanie wywołać metodę reproduce i gainEnergy w sposób specyficzny dla każdego z tych typów. Zresztą widać to w działaniu samego programu:
Tym przykładem kończymy dzisiejszą lekcję. W następnym wpisie z serii omówimy czym jest ostatni z filarów: abstrakcja. Udanej środy i wysokich wyników z matur