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.