Podstawy programowania obiektowego #2 – Abstrakcja i dziedziczenie w programowaniu obiektowym

Dziedziczenie w programowaniu obiektowym i stosowanie poziomów abstrakcji to podstawowe mechanizmy tworzenia software’u. Pozwalają nam nie tylko na schludną organizację kodu, ale też umożliwiają bliskie odwzorowanie rzeczywistości.

Rozejrzyj się wokół siebie i wybierz jeden przedmiot ze swojego otoczenia. Jestem pewien, że potrafisz wskazać co będzie dla niego wyższą i niższą warstwą abstrakcji. Wyobraź sobie granat ręczny (tak, wiem, każdy w otoczeniu ma jakiś granat). W jedną stronę możemy zauważyć, że granat taki jest jakimś rodzajem broni, a w drugą – że możemy wydzielić różne typy granatów (np. odłamkowy, dymny, ogłuszający).

To właśnie są różne warstwy abstrakcji, a programowanie obiektowe ma mechanizmy, które pomogą nam to odwzorować.

Klasy abstrakcyjne

Załóżmy, że tworzymy grę i naszym aktualnym zadaniem jest zakodowanie obsługi broni. Każda broń ma jakieś wspólne atrybuty (np. waga broni, zadawane obrażenia) i funkcjonalność (np. wydawanie dźwięku przy ataku).  Raczej jednak w żadnym wypadku nie będziemy chcieli stworzyć obiektu klasy Bron, a obiekty bardziej szczegółowe np. klasy Pistolet, GranatOdlamkowy itp. Tutaj do akcji wkraczają klasy abstrakcyjne. Są to takie klasy, dla których nie możemy stworzyć instancji obiektu, a są one jedynie uogólnieniem dla klas potomnych. Jeśli najlepiej rozumujesz na przykładach kodu – wstrzymaj się jeszcze chwilę, po kilku słowach o dziedziczeniu przytoczę przykład w Javie.

Hierarchia dziedziczenia

Dziedziczenie jest najczęściej stosowanym sposobem na rozszerzanie funkcjonalności danej klasy i szeregowanie klas w hierarchię. Jeśli klasa KlasaB dziedziczy z klasy KlasaA, to oznacza, że posiada wszystkie jej atrybuty oraz metody (z zastrzeżeniem enkapsulacji, o czym później). Może również tę odziedziczoną funkcjonalność rozszerzać poprzez dodanie nowych właściwości i metod, ale też nadpisać zachowanie klasy nadrzędnej poprzez przysłonienie (ang. override) metody (znów z zastrzeżeniem metod prywatnych).

Uwaga! W Javie nie istnieje dziedziczenie wielokrotne, tj. klasa nie może dziedziczyć po wielu różnych klasach. Niektóre języki programowania (np. C++ czy Perl) pozwalają na takie konstrukcje.

Kontynuując nasz przykład broni – aby stworzyć klasy Pistolet i KarabinMaszynowy możemy działać następująco:

  1. Tworzymy klasę Bron – ma ona np. atrybut zadawaneObrazenia, ponieważ jest to wspólne dla wszystkich broni. Może również mieć abstrakcyjną metodę atakuj() – wiemy, że każdą bronią możemy atakować, ale na tym poziomie abstrakcji nie wiemy jak. Oprócz tego dorzućmy jeszcze metodę wydajDzwiekAtaku(). Ta metoda nie będzie abstrakcyjna, a będzie miała domyślną implementację naśladującą dźwięk „ŁUP” – w ten sposób domyślnie każda broń będzie łupać 🙂 Pamiętaj, że dla klasy abstrakcyjnej nie możemy utworzyć instancji obiektu!
  2. Tworzymy klasę BronPalna, która dziedziczy z klasy Bron. Możemy rozszerzyć jej funkcjonalność dodając metodę strzel() (każda broń palna przecież z definicji – strzela). O ile klasa BronPalna nie będzie abstrakcyjna, to musimy również zaimplementować metodę atakuj(), która w tym wypadku może wywołać strzel() i jakieś dodatkowe akcje. Nadpiszmy również metodę wydajDzwiekAtaku(), która w tym przypadku zrobi „Pif, Paf!”, co będzie domyślnym zachowaniem broni palnej.
  3. Tworzymy klasę Pistolet. Załóżmy, że nie chcemy zmieniać jej zachowania względem broni palnej – rozszerzamy więc klasę BronPalna i zostawiamy implementację nienaruszoną. W przypadku Karabinu chcemy zrobić „Tratatata!” przy wystrzale 🙂 więc nadpisujemy metodę wydajDzwiekAtaku().

Poniżej realizacja w języku Java. Przykład dość długi, ale chyba dobrze obrazujący hierarchię dziedziczenia.

public abstract class Bron {
    protected int zadawaneObrazenia;

    public Bron(int zadawaneObrazenia) {
        this.zadawaneObrazenia = zadawaneObrazenia;
    }

    public abstract void atakuj();

    public void wydajDzwiekAtaku() {
        System.out.println("ŁUP");
    };
}

public class BronPalna extends Bron {
    public BronPalna(int zadawaneObrazenia) {
        super(zadawaneObrazenia);
    }

    @Override
    public void atakuj() {
        strzel();
        wydajDzwiekAtaku();
        //Dodatkowe czynności, np. odrzut po strzale
    }

    public void strzel(){
        //Tu jest mechanizm strzału
    }

    @Override
    public void wydajDzwiekAtaku() {
        System.out.println("Pif, Paf!");
    }
}

public class Pistolet extends BronPalna{
    public Pistolet(int zadawaneObrazenia) {
        super(zadawaneObrazenia);
    }
}

public class KarabinMaszynowy extends BronPalna{
    public KarabinMaszynowy(int zadawaneObrazenia) {
        super(zadawaneObrazenia);
    }

    @Override
    public void wydajDzwiekAtaku() {
        System.out.println("Tratatata!");
    }
}

Dlaczego w każdej klasie dodałem konstruktor przyjmujący zadawaneObrazenia? A to dlatego, żeby wymusić na użytkowniku zainicjowanie tej właściwości w każdym przypadku. Można to oczywiście również rozwiązać poprzez ustawienie wartości domyślnej na poziomie klasy Bron i liczyć, że użytkownik będzie pamiętał o jej ustawieniu. Dziedziczenie w programowaniu obiektowym jest tak potężnym mechanizmem, że problemy tego typu często możemy rozwiązać na co najmniej dwa albo więcej sposobów.

Metody, które są przysłaniane w Javie technicznie nie wymagają adnotacji @Override – zwiększa to jedynie czytelność, zwracając naszą uwagę na to, że metoda nadpisuje funkcjonalność klasy bazowe.

Wyobraź sobie teraz poniższy kawałek kodu:

BronPalna ogolnaBronPalna = new BronPalna(5);
Pistolet pistolet = new Pistolet(6);
KarabinMaszynowy karabin = new KarabinMaszynowy(7);

ogolnaBronPalna.wydajDzwiekAtaku();
pistolet.wydajDzwiekAtaku();
karabin.wydajDzwiekAtaku();

Czy wiesz, jaki będzie rezultat jego wykonania?

Rezultat wykonania powyższego kodu

Pif, Paf! Pif, Paf! Tratatata!

[collapse]

Nie było to pewnie zbyt trudne, ale w przystępny sposób pokazuje działanie nadpisanych metod w hierarchii dziedziczenia.

Przykład bardziej techniczny

W jakich mechanizmach możemy spotkać abstrakcje w codziennej pracy programisty? Przykładem może być na przykład realizacja operacji na bazach danych – możemy stworzyć abstrakcyjny twór, który zapisuje i pobiera dane z bazy oraz konkretne usługi działające dla różnych typów DB, na przykład Postgres, MySQL czy Oracle. W ten sposób abstrakcja zwiększa nam łatwość użycia niektórych mechanizmów (operując na wyższym poziomie abstrakcji nie musimy wnikać jak działa głębsza implementacja). Zwiększa też bezpieczeństwo kodu w trakcie modyfikacji – przy poprawnej architekturze nie powinniśmy mieć konieczności zmiany istniejącego kodu, a powinno nam wystarczyć jego rozszerzenie o nowe funkcjonalności.

Podsumowując

  • Abstrakcja jest formą uproszczenia problemu, która polega na korzystaniu z uproszczonego zestawu cech danego obiektu, niezależnie od szczegółowej implementacji.
  • Celem abstrakcji może być ułatwienie rozwiązania jakiegoś problemu lub bardziej ogólne do niego podejście.
  • W programowaniu obiektowym klasa abstrakcyjna najczęściej oznacza klasę dla której nie możemy utworzyć instancji obiektów. Metody abstrakcyjne to takie metody, które nie posiadają domyślnej implementacji i muszą zostać zaimplementowane w momencie tworzenia klasy dziedziczącej.
  • Mechanizm dziedziczenia pozwala na rozszerzenie lub nadpisanie funkcjonalności danej klasy, jednocześnie pozwalając programiście na korzystanie z wielu warstw abstrakcji – od najbardziej ogólnych interfejsów do szczegółowej implementacji.
  • Masz ochotę sprawdzić swoją wiedzę poprzez wykonanie pracy domowej?
    • Napisz program, który będzie zawierał abstrakcyjną klasę Pracownik i klasy pochodne PracownikFizyczny, PracownikBiurowy i Dyrektor. Niech każdy z nich posiada metodę wyliczWyplate() która w różny sposób będzie liczyła wysokość wypłaty dla różnych „stanowisk”.

 

Poprzedni artykuł: Podstawy programowania obiektowego #1 : Klasa i obiekt
Następny artykuł: Podstawy programowania obiektowego #3 : Interfejsy

Zobacz więcej (spis treści): Kurs programowania obiektowego #0: Wstęp i spis treści

Podsumowanie
Tytuł artykułu
Abstrakcja i dziedziczenie w programowaniu obiektowym
Opis
Dowiedz się jak działa dziedziczenie w programowaniu obiektowym i jak możemy je wykorzystać, żeby dokładnie odwzorować rzeczywiste zależności.
Autor

Leave a Comment

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