Dziedziczenie w programowaniu – ekstremalny przypadek

przykładowy schemat

Z dziedziczeniem spotykamy się często w programowaniu, w szczególności w programowaniu obiektowym. Powstaje pytanie: Kiedy zastosować dziedziczenie a kiedy agregację? W tym artykule zostanie przedstawiony ekstremalny przypadek, który nadużywa mechanizmu dziedziczenia.

Przypadek zostanie przedstawiony na podstawie rzeczywistego projektu – gry komputerowej, którą napisałem w 2011 roku. Do implementacji gry wykorzystałem język C++. Przedstawione fragmenty kodu będą zatem w języku C++. W innych językach programowania (np. Java) mechanizm dziedziczenia jest zbliżony, zatem przedstawione fragmenty kodu można również odnieść do innych języków. Poniżej znajduje się zrzut ekranu z jednego z poziomów gry.

zrzut ekranu z gry Strong Invader
Napisana gra. Gracz broni Ziemię przed atakiem wroga. Grafika – poziom Paint Master 🙂

Komentarz na temat nazewnictwa przed przejściem do kolejnej części artykułu. Kod programu pochodzi z 2011 roku i jest napisany w języku angielskim i polskim. Stosowanie różnych języków w nazewnictwie nie jest zalecaną praktyką. Temat ten wybiega poza obszar tego artykułu, stąd tutaj krótka informacja – nie warto stosować w nazewnictwie zarówno języka polskiego oraz angielskiego. Warto zastosować jedną konwencję.

Gra składa się z 5 poziomów. W kodzie źródłowym została zaimplementowana klasa bazowa o nazwie poziomy_abstrakcja zawierająca dane oraz metody wykorzystywane do interakcji z grą. Poniżej znajduje się definicja wybranych składników tej klasy:

class poziomy_abstrakcja
{
    public:
        BITMAP* statek_wrog_1;
        BITMAP* statek_wrog_2;
        BITMAP* celownik;
        BITMAP* plus_1;
        volatile int strzal_x;
        volatile int strzal_y;

        // ... ciag dalszy

        int przesun_w_dol();
        virtual void rysuj_przeszkody();
        virtual void rysuj_linie_strzalu(int stat_x, int stat_y);
        void generowanie_wrogow();

        // ... ciag dalszy
};

Dokładne znaczenie każdego pola oraz każdej metody nie jest istotne w tym artykule. Następnie w projekcie mamy zdefiniowaną klasę o nazwie przeciwnik_poz_1, która zawiera elementy wykorzystywane do interakcji z grą na poziomie pierwszym. Klasa przeciwnik_poz_1 dziedziczy po klasie poziomy_abstrakcja:

class przeciwnik_poz_1 : public poziomy_abstrakcja
{
    // ... skladniki klasy
};

Na tym etapie możemy stwierdzić, że dziedziczenie może mieć tutaj uzasadnione użycie. Klasa reprezentująca poziom pierwszy dziedziczy po klasie reprezentującej bazowe dane oraz bazowe metody gry. Następnie w projekcie mamy zdefiniowaną klasę o nazwie przeciwnik_poz_2, która zawiera elementy wykorzystywane do interakcji z grą na poziomie drugim. Klasa przeciwnik_poz_2 dziedziczy po klasie przeciwnik_poz_1:

class przeciwnik_poz_2 : public przeciwnik_poz_1
{
    // ... skladniki klasy
};

Klasa przeciwnik_poz_2 dziedziczy elementy klasy bazowej przeciwnik_poz_1, które są specyficzne tylko dla poziomu pierwszego gry. Nie możemy stwierdzić, że klasa przeciwnik_poz_2 jest rodzajem klasy przeciwnik_poz_1. W tym momencie możemy powiedzieć, że doszło tutaj do nadużycia mechanizmu dziedziczenia. Następnie w projekcie mamy zdefiniowane kolejne klasy, które analogicznie dziedziczą po sobie:

class przeciwnik_poz_3 : public przeciwnik_poz_2
{
    // ... skladniki klasy
};

class przeciwnik_poz_4 : public przeciwnik_poz_3
{
    // ... skladniki klasy
};

class przeciwnik_poz_5 : public przeciwnik_poz_4
{
    // ... skladniki klasy
};

Ostatecznie otrzymaliśmy długą hierarchię dziedziczenia klas. Poniższy schemat przedstawia otrzymaną hierarchię:

dziedziczenie w programowaniu - ekstremalny przypadek, schemat
Schemat dziedziczenia klas

Przypadek z rzeczywistego projektu przedstawia nadużycie mechanizmu dziedziczenia. W projekcie powinna zostać zastosowana agregacja.

Podsumowanie:

  • zastosowanie długich hierarchii dziedziczenia klas komplikuje czytelność projektu,
  • zastosowanie długich hierarchii dziedziczenia klas może oznaczać błąd w projekcie systemu,
  • jeżeli klasa A jest rodzajem klasy B to może być uzasadnione zastosowanie dziedziczenia pomiędzy tymi klasami,
  • jeżeli klasa A nie jest rodzajem klasy B to powinniśmy skorzystać z agregacji zamiast z dziedziczenia.

Dziękuję za przeczytanie artykułu 🙂.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *