Polimorfizm
O tym, że poziom nowych inżynierów z roku na rok co raz bardziej spada mówi się już od jakiegoś czasu. Ba! świadomi są tego nawet absolwenci i studenci ostatnich lat. Zawsze jednak myślałem, że jest to stwierdzenie nieco przesadzone, z pokroju tych, że niegdyś trawa była bardziej zielona, a niebo bardziej niebieskie. Tymczasem dowiaduję się (z różnych źródeł), że często na rozmowy kwalifikacyjne na stanowisko programisty przychodzą ludzie bez kompletnie żadnych kwalifikacji, którzy często nawet nie mają zielonego pojęcia o składni języków, które mają wpisane w CV. W związku z tym problem sprawia im stworzenie na szybko nawet najprostszych programów pokroju “Hello world”, że już nie wspomnę o typowym zadaniu implementacji ciągu Fibonacciego. Jeśli już wiesz co to jest polimorfizm - masz naprawdę spore szanse na przyjęcie.
Odpowiedzmy sobie zatem na pytanie, które, jeśli wierzyć temu co napisałem w poprzednim akapicie, pozwoli zdobyć dobrą pracę i zadziwić kolegów: “czym zatem jest polimorfizm”?
W programowaniu obiektowym wyróżniamy trzy podstawowe jednostki: klasę, obiekt i typ. Celowo odchodzę tu od utartego schematu klasa - obiekt w celu lepszego zobrazowania drobnych niuansów w programowaniu obiektowym, które są niezbędne dla zrozumienia idei polimorfizmu. Ważne jest, żeby nie dać się omamić “kotkami”, “pieskami” i “zwierzątkami” z wielu poradników, które całkiem dobrze reprezentują zależność klasa - obiekt, jednak wykładają się w przypadku próby prezentacji nieco bardziej skomplikowanych mechanizmów języków programowania niż dziedziczenie jednokrotne. Ciężko bowiem wyobrazić sobie co powstanie z rzutowania kota na psa lub na odwrót (babochłop?). Znacznie lepsze od takiego podejścia jest wyobrażenie sobie metod dostępu do obiektów, przed którymi postawione są pewne zadania - czyli interfejsu dostępu do nich.
Wracając do obiektowego trójpodziału, który zaproponowałem: jeśli mielibyśmy się poruszać po kolejnych poziomach abstrakcji, wówczas typ byłby tworem najbardziej abstrakcyjnym, klasa stałaby po środku, a obiekt byłby tworem najbardziej konkretnym, umożliwiającym dostęp do metod interfejsu dla danego typu. Dla owego obiektu określona jest klasa będąca implementacją (lub nawet rozszerzeniem) interfejsu. Klasa sama w sobie nie stanowi jednak typu danego obiektu - jest to mylące, szczególnie w językach typu C++, gdyż w momencie konstruowania obiektu często określamy jego typ, który posiada taką samą nazwę co klasa. W związku z zaproponowanym trójpodziałem możemy posiadać różne implementacje interfejsu dla danego typu.
Na przykład tworząc edytor plików możemy zechcieć stworzyć typ MyFile
, a w
którym zdefiniowane byłyby sposoby dostępu do plików. Metoda open()
otwierałaby binarnie wszystkie pliki i zapisywała je w pamięci, zaś metoda
printContent()
powodowałaby prawidłowe wyświetlenie tak otwartego pliku na
ekranie. Ponieważ mamy różne rodzaje plików, które moglibyśmy chcieć edytować,
metoda ta musiałaby być zaimplementowana w różny sposób. Oczywiste jest
przecież, że zawartość plików pdf jest inna niż plików txt, a te z kolei
wymagają innego podejścia niż standard OpenDocument. W momencie wyświetlania
konkretna implementacja sposobu wyświetlania tych rodzajów plików na ekranie
jest jednak przed nami przesłonięta - dysponujemy jednolitym, bardzo wygodnym
interfejsem dostępu do plików.
Naiwna implementacja w języku C++ takiego otwierania i wyświetlania plików mogłaby wyglądać następująco:
int main(int argc, char* argv[]) {
std::string filepath;
std::string extension;
MyFile * file;
for( int i = 1; i < argc ++i ) {
filepath = argv[i];
extension = filepath.substr(filepath.length() - 3);
if( extension == "pdf" )
file = new MyPdf;
else if( extension == "odt" )
file = new MyOdt;
else
file = new MyAsciiFile;
file->open(filepath); // niech to bedzie operacja w miejscu (in-place)
file->printContent();
delete file;
}
}
W powyższym kodzie został utworzony wskaźnik na typ MyFile
, a następnie
dynamicznie (przy pomocy operatora new
) są tworzone obiekty tego typu. Przy
tej okazji warto pamiętać, że jeśli ich macierzyste podtypy (a więc na przykład
MyPdf
) rozszerzają w jakiś sposób interfejs (na przykład przez dodanie metody
printPage(unsigned pageNo)
, to ich wywołanie nie będzie możliwe, ponieważ
cały czas obsługujemy interfejs typu MyFile
. Do nowych metod można by się
spróbować dobrać na kilka sposobów, a jednym z nich jest na przykład rzutowanie
obiektu typu MyFile
w dół.
Polimorfizm jest więc wspaniałym mechanizmem umożliwiającym dynamiczną zmianę konkretnego zachowania interfejsu w zależności na przykład od informacji, którymi dysponujemy.