Lekcja 26 (OOP 6): Dziedziczenie (extends), Nadpisywanie Metod, Słowo Kluczowe final

Witaj w szóstej lekcji naszego modułu o Programowaniu Obiektowym w PHP! W poprzednich lekcjach poznaliśmy podstawy OOP, nauczyliśmy się tworzyć klasy i obiekty, zarządzać dostępem do ich składowych za pomocą modyfikatorów dostępu oraz korzystać ze składowych statycznych. Dzisiaj zajmiemy się jednym z najważniejszych i najpotężniejszych mechanizmów OOP: dziedziczeniem (inheritance). Dziedziczenie pozwala na tworzenie nowych klas (nazywanych klasami potomnymi lub podklasami) na bazie istniejących klas (nazywanych klasami bazowymi, nadrzędnymi lub nadklasami), co promuje reużywalność kodu i tworzenie hierarchii klas.

Omówimy, jak w PHP implementuje się dziedziczenie za pomocą słowa kluczowego extends, jak klasy potomne dziedziczą właściwości i metody po klasach bazowych, jak można nadpisywać (override) odziedziczone metody, aby dostosować ich zachowanie, oraz jak używać słowa kluczowego parent:: do wywoływania metod z klasy nadrzędnej. Na koniec przyjrzymy się słowu kluczowemu final, które pozwala zapobiegać dziedziczeniu klas lub nadpisywaniu metod.

Czym Jest Dziedziczenie?

Dziedziczenie to mechanizm, który pozwala jednej klasie (klasie potomnej) przejmować (dziedziczyć) właściwości i metody z innej klasy (klasy bazowej). Klasa potomna może następnie dodawać własne, unikalne właściwości i metody, a także modyfikować (nadpisywać) zachowanie odziedziczonych metod. Tworzy to relację "jest rodzajem" (is-a relationship) – na przykład, jeśli mamy klasę Pojazd i klasę Samochod, która dziedziczy po Pojazd, to możemy powiedzieć, że "Samochód jest rodzajem Pojazdu".

Główne korzyści z dziedziczenia:

W PHP klasa może dziedziczyć tylko po jednej klasie bazowej – PHP nie wspiera wielokrotnego dziedziczenia klas (choć pewne aspekty wielokrotnego dziedziczenia można osiągnąć za pomocą cech – traits, o których powiemy w kolejnej lekcji).

Implementacja Dziedziczenia za Pomocą extends

Aby zadeklarować, że jedna klasa dziedziczy po innej, używamy słowa kluczowego extends.

<?php
// Klasa bazowa (nadrzędna)
class Zwierze
{
    public string $nazwa;
    protected int $wiek = 0;

    public function __construct(string $nazwa)
    {
        $this->nazwa = $nazwa;
        echo "Utworzono obiekt Zwierze: " . $this->nazwa . "<br>";
    }

    public function jedz(string $pokarm): void
    {
        echo $this->nazwa . " je " . $pokarm . ".<br>";
    }

    public function spij(): void
    {
        echo $this->nazwa . " śpi.<br>";
    }

    public function getWiek(): int
    {
        return $this->wiek;
    }

    public function setWiek(int $wiek): void
    {
        if ($wiek >= 0) {
            $this->wiek = $wiek;
        } else {
            echo "Wiek nie może być ujemny.<br>";
        }
    }
}

// Klasa potomna (podklasa) dziedzicząca po Zwierze
class Pies extends Zwierze
{
    public string $rasa;

    // Konstruktor klasy potomnej
    public function __construct(string $nazwa, string $rasa)
    {
        // Wywołanie konstruktora klasy bazowej (Zwierze)
        parent::__construct($nazwa); // `parent::` odnosi się do klasy nadrzędnej
        $this->rasa = $rasa;
        echo "Utworzono obiekt Pies rasy: " . $this->rasa . "<br>";
    }

    // Nowa metoda specyficzna dla klasy Pies
    public function szczekaj(): void
    {
        echo $this->nazwa . " (rasa: " . $this->rasa . ") szczeka: Hau hau!<br>";
    }

    // Nadpisywanie metody jedz z klasy Zwierze
    public function jedz(string $pokarm): void
    {
        if ($pokarm === "czekolada") {
            echo $this->nazwa . " nie może jeść czekolady! To niezdrowe dla psów.<br>";
        } else {
            // Wywołanie oryginalnej metody jedz z klasy Zwierze
            parent::jedz($pokarm);
            echo $this->nazwa . " merda ogonem, bo dostał " . $pokarm . ".<br>";
        }
    }
}

$azor = new Pies("Azor", "Owczarek Niemiecki");
$azor->setWiek(5);

// Metody odziedziczone z Zwierze
$azor->jedz("karma dla psów");
$azor->jedz("czekolada"); // Wywoła nadpisaną metodę
$azor->spij();
echo $azor->nazwa . " ma " . $azor->getWiek() . " lat.<br>";

// Metoda specyficzna dla Pies
$azor->szczekaj();

echo "<hr>";

$mruczek = new Zwierze("Mruczek"); // Zwykły obiekt klasy bazowej
$mruczek->jedz("ryba");
// $mruczek->szczekaj(); // BŁĄD! Fatal error: Call to undefined method Zwierze::szczekaj()
?>

W tym przykładzie:

Co Jest Dziedziczone?

Klasa potomna dziedziczy:

Nadpisywanie Metod (Method Overriding)

Nadpisywanie metod to możliwość zdefiniowania w klasie potomnej metody o tej samej nazwie i takiej samej sygnaturze (lub kompatybilnej sygnaturze) co metoda w klasie bazowej. Gdy metoda jest wywoływana na obiekcie klasy potomnej, wykonana zostanie wersja metody z klasy potomnej, a nie z klasy bazowej.

Zasady nadpisywania metod:

W przykładzie z klasą Pies, metoda jedz() została nadpisana:

<?php
// ... (definicja klasy Zwierze jak wyżej)
class Pies extends Zwierze
{
    // ... (reszta klasy Pies)

    // Nadpisywanie metody jedz z klasy Zwierze
    public function jedz(string $pokarm): void // Ta sama nazwa i sygnatura (lub kompatybilna)
    {
        if ($pokarm === "czekolada") {
            echo $this->nazwa . " nie może jeść czekolady! To niezdrowe dla psów.<br>";
        } else {
            // Wywołanie oryginalnej metody jedz z klasy Zwierze
            parent::jedz($pokarm);
            echo $this->nazwa . " merda ogonem, bo dostał " . $pokarm . ".<br>";
        }
    }
}
?>

Gdy wywołamy $azor->jedz("karma dla psów"), wykona się kod z nadpisanej metody w klasie Pies.

Słowo Kluczowe parent::

Słowo kluczowe parent:: pozwala na dostęp do składowych (metod i właściwości statycznych, a także konstruktora) klasy nadrzędnej z wnętrza klasy potomnej.

parent:: odnosi się zawsze do bezpośredniego rodzica w hierarchii dziedziczenia.

Słowo Kluczowe final

Słowo kluczowe final może być użyte w dwóch kontekstach:

1. Metody Finalne (final function)

Jeśli metoda w klasie bazowej jest zadeklarowana jako final, oznacza to, że nie może być nadpisana w żadnej klasie potomnej. Próba nadpisania metody finalnej spowoduje błąd krytyczny (Fatal error).

<?php
class Urzadzenie
{
    public final function getNumerSeryjny(): string
    {
        // Logika pobierania unikalnego numeru seryjnego, która nie powinna być zmieniana
        return "SN-" . uniqid();
    }

    public function wlacz(): void
    {
        echo "Urzadzenie włączone.<br>";
    }
}

class Komputer extends Urzadzenie
{
    // Próba nadpisania metody finalnej - spowoduje błąd
    /*
    public function getNumerSeryjny(): string 
    {
        return "KOMP-" . parent::getNumerSeryjny();
    }
    */ // To by spowodowało: Fatal error: Cannot override final method Urzadzenie::getNumerSeryjny()

    // Nadpisanie metody niefinalnej jest dozwolone
    public function wlacz(): void
    {
        parent::wlacz();
        echo "System operacyjny komputera startuje...<br>";
    }
}

$mojKomputer = new Komputer();
echo "Numer seryjny komputera: " . $mojKomputer->getNumerSeryjny() . "<br>";
$mojKomputer->wlacz();
?>

Metody finalne są używane, gdy chcemy zagwarantować, że pewna kluczowa część implementacji klasy nie zostanie zmieniona przez klasy potomne, np. ze względów bezpieczeństwa lub integralności.

2. Klasy Finalne (final class)

Jeśli cała klasa jest zadeklarowana jako final, oznacza to, że nie może być ona dziedziczona. Żadna inna klasa nie może użyć jej jako klasy bazowej (nie może po niej extends).

<?php
final class StringUtils
{
    // Klasa z metodami pomocniczymi, nieprzeznaczona do dziedziczenia
    public static function odwracaj(string $tekst): string
    {
        return strrev($tekst);
    }
}

// Próba dziedziczenia po klasie finalnej - spowoduje błąd
/*
class MojeNarzędziaString extends StringUtils 
{
    // ...
}
*/ // To by spowodowało: Fatal error: Class MojeNarzędziaString may not inherit from final class (StringUtils)

echo StringUtils::odwracaj("Hello World") . "<br>";
?>

Klasy finalne są używane, gdy projektant klasy chce jawnie zabronić jej rozszerzania, np. dla klas narzędziowych (utility classes) lub klas, których wewnętrzna spójność mogłaby zostać naruszona przez dziedziczenie.

Konstruktory i Dziedziczenie

Jak wspomniano wcześniej, jeśli klasa potomna definiuje własny konstruktor, konstruktor klasy bazowej nie jest automatycznie wywoływany. Aby go wywołać, należy użyć parent::__construct(...).

Dlaczego to jest ważne? Konstruktor klasy bazowej często wykonuje kluczowe inicjalizacje (np. ustawia odziedziczone właściwości). Jeśli go nie wywołamy, obiekt może nie być w pełni lub poprawnie zainicjalizowany.

Co jeśli klasa potomna nie ma własnego konstruktora? W takim przypadku, jeśli tworzymy obiekt klasy potomnej, automatycznie zostanie wywołany konstruktor z najbliższej klasy nadrzędnej w hierarchii, która posiada konstruktor. Jeśli żadna klasa w hierarchii (aż do samej góry) nie ma konstruktora, to nic specjalnego się nie dzieje (chyba że próbujemy przekazać argumenty do new, co może spowodować błąd, jeśli nie ma pasującego konstruktora).

<?php
class A
{
    public function __construct()
    {
        echo "Konstruktor A<br>";
    }
}

class B extends A
{
    // Brak własnego konstruktora, więc wywoła się konstruktor A
}

class C extends A
{
    public function __construct()
    {
        // parent::__construct(); // Jeśli odkomentujemy, wywoła się też konstruktor A
        echo "Konstruktor C<br>";
    }
}

class D extends B // B dziedziczy po A
{
    public function __construct()
    {
        parent::__construct(); // Wywoła konstruktor B, który (ponieważ nie ma własnego) wywoła konstruktor A
        echo "Konstruktor D<br>";
    }
}


echo "Tworzę obiekt B:<br>";
$objB = new B(); // Wywoła konstruktor A

echo "<hr>Tworzę obiekt C:<br>";
$objC = new C(); // Wywoła tylko konstruktor C (chyba że C jawnie wywoła parent::__construct())

echo "<hr>Tworzę obiekt D:<br>";
$objD = new D(); // Wywoła konstruktor A (przez B), potem D
?>

Zawsze dobrą praktyką jest jawne wywoływanie parent::__construct() w konstruktorze klasy potomnej, jeśli klasa bazowa ma konstruktor, który powinien być wykonany.

Podsumowanie Lekcji

W tej lekcji zgłębiliśmy mechanizm dziedziczenia w PHP. Nauczyliśmy się, jak używać słowa kluczowego extends do tworzenia klas potomnych, które dziedziczą publiczne i chronione składowe po klasach bazowych. Zrozumieliśmy, jak nadpisywać metody, aby dostosować zachowanie w klasach potomnych, oraz jak używać parent:: do odwoływania się do implementacji z klasy nadrzędnej, w szczególności do jej konstruktora.

Omówiliśmy również słowo kluczowe final, które pozwala na tworzenie metod finalnych (niemożliwych do nadpisania) oraz klas finalnych (niemożliwych do dziedziczenia), co daje większą kontrolę nad projektem hierarchii klas. Podkreśliliśmy znaczenie prawidłowego zarządzania konstruktorami w kontekście dziedziczenia.

Dziedziczenie jest potężnym narzędziem, które, gdy jest stosowane właściwie, prowadzi do bardziej zorganizowanego, elastycznego i reużywalnego kodu. W następnej lekcji przyjrzymy się klasom abstrakcyjnym i metodom abstrakcyjnym, które są ściśle związane z dziedziczeniem i pozwalają na definiowanie "szablonów" dla klas potomnych.


Zadanie praktyczne

Stwórz hierarchię klas dla różnych typów figur geometrycznych.

  1. Stwórz klasę bazową FiguraGeometryczna z:
    • Chronioną właściwością $nazwaFigury.
    • Konstruktorem przyjmującym nazwę figury.
    • Publiczną metodą getNazwa(): string zwracającą nazwę.
    • Publiczną metodą obliczPole(): float, która na razie zwraca 0.0 (będzie nadpisywana).
    • Publiczną metodą obliczObwod(): float, która na razie zwraca 0.0 (będzie nadpisywana).
  2. Stwórz klasę Kolo dziedziczącą po FiguraGeometryczna:
    • Z prywatną właściwością $promien.
    • Konstruktorem przyjmującym promień, który wywołuje konstruktor rodzica z nazwą "Koło".
    • Nadpisz metodę obliczPole() (pole koła: π * r^2). Możesz użyć M_PI dla wartości π.
    • Nadpisz metodę obliczObwod() (obwód koła: 2 * π * r).
  3. Stwórz klasę Prostokat dziedziczącą po FiguraGeometryczna:
    • Z prywatnymi właściwościami $bokA i $bokB.
    • Konstruktorem przyjmującym długości boków, który wywołuje konstruktor rodzica z nazwą "Prostokąt".
    • Nadpisz metodę obliczPole() (pole prostokąta: a * b).
    • Nadpisz metodę obliczObwod() (obwód prostokąta: 2*a + 2*b).
  4. Przetestuj klasy: utwórz obiekty Kolo i Prostokat, a następnie wyświetl ich nazwy, pola i obwody.

Kliknij, aby zobaczyć przykładowe rozwiązanie
<?php
class FiguraGeometryczna
{
    protected string $nazwaFigury;

    public function __construct(string $nazwa)
    {
        $this->nazwaFigury = $nazwa;
    }

    public function getNazwa(): string
    {
        return $this->nazwaFigury;
    }

    public function obliczPole(): float
    {
        return 0.0;
    }

    public function obliczObwod(): float
    {
        return 0.0;
    }
}

class Kolo extends FiguraGeometryczna
{
    private float $promien;

    public function __construct(float $promien)
    {
        parent::__construct("Koło");
        $this->promien = $promien;
    }

    public function obliczPole(): float
    {
        return M_PI * pow($this->promien, 2);
    }

    public function obliczObwod(): float
    {
        return 2 * M_PI * $this->promien;
    }
}

class Prostokat extends FiguraGeometryczna
{
    private float $bokA;
    private float $bokB;

    public function __construct(float $a, float $b)
    {
        parent::__construct("Prostokąt");
        $this->bokA = $a;
        $this->bokB = $b;
    }

    public function obliczPole(): float
    {
        return $this->bokA * $this->bokB;
    }

    public function obliczObwod(): float
    {
        return 2 * $this->bokA + 2 * $this->bokB;
    }
}

// Testowanie
$mojeKolo = new Kolo(5.0);
echo "Figura: " . $mojeKolo->getNazwa() . "<br>";
echo "Pole: " . round($mojeKolo->obliczPole(), 2) . "<br>";
echo "Obwód: " . round($mojeKolo->obliczObwod(), 2) . "<br>";

echo "<hr>";

$mojProstokat = new Prostokat(4.0, 7.0);
echo "Figura: " . $mojProstokat->getNazwa() . "<br>";
echo "Pole: " . round($mojProstokat->obliczPole(), 2) . "<br>";
echo "Obwód: " . round($mojProstokat->obliczObwod(), 2) . "<br>";

/* Przykładowy wynik:
Figura: Koło
Pole: 78.54
Obwód: 31.42
---
Figura: Prostokąt
Pole: 28
Obwód: 22
*/
?>

Zadanie do samodzielnego wykonania

Rozwiń hierarchię klas pojazdów.

  1. Stwórz klasę bazową Pojazd z właściwościami (np. protected string $marka, protected string $model, protected int $rokProdukcji) i metodami (np. public function getInfo(): string, public function uruchomSilnik(): void).
  2. Stwórz klasę SamochodOsobowy dziedziczącą po Pojazd, dodaj właściwość np. private int $liczbaDrzwi i nadpisz metodę getInfo(), aby uwzględniała liczbę drzwi. Dodaj konstruktor.
  3. Stwórz klasę Motocykl dziedziczącą po Pojazd, dodaj właściwość np. private string $typRamy (np. "cruiser", "sportowy") i nadpisz metodę getInfo(). Dodaj konstruktor.
  4. W metodzie uruchomSilnik() w klasie Pojazd niech wyświetla ogólny komunikat. W klasie Motocykl nadpisz tę metodę, aby wyświetlała bardziej charakterystyczny dźwięk dla motocykla.
  5. Przetestuj, tworząc obiekty obu klas potomnych i wywołując ich metody.


FAQ - Dziedziczenie, Nadpisywanie, final

Czy klasa potomna dziedziczy konstruktor klasy bazowej?

Jeśli klasa potomna nie definiuje własnego konstruktora, to dziedziczy i używa konstruktora klasy bazowej (jeśli istnieje). Jeśli klasa potomna definiuje własny konstruktor, konstruktor bazowy nie jest automatycznie wywoływany – trzeba go jawnie wywołać za pomocą parent::__construct(), jeśli jest taka potrzeba.

Co się stanie, jeśli spróbuję uzyskać dostęp do prywatnej właściwości klasy bazowej z klasy potomnej?

Spowoduje to błąd (Notice: Undefined property lub Fatal error, w zależności od kontekstu i wersji PHP), ponieważ prywatne składowe są dostępne tylko wewnątrz klasy, która je zdefiniowała. Klasa potomna nie ma do nich bezpośredniego dostępu. Dostęp powinien odbywać się poprzez publiczne lub chronione metody klasy bazowej.

Czy mogę dziedziczyć po wielu klasach w PHP (wielokrotne dziedziczenie)?

Nie, PHP nie wspiera wielokrotnego dziedziczenia klas (jedna klasa może extends tylko jedną inną klasę). Problem "diamentowy" i złożoność zarządzania hierarchią są głównymi powodami. Jednakże, od PHP 5.4 można używać Cech (Traits) do reużywania kodu i horyzontalnego współdzielenia funkcjonalności między klasami, co w pewnym stopniu adresuje potrzebę wielokrotnego dziedziczenia zachowań.

Kiedy powinienem użyć final dla klasy lub metody?

Użyj final dla metody, jeśli chcesz mieć pewność, że jej implementacja nie zostanie zmieniona przez żadną klasę potomną (np. dla krytycznych operacji lub algorytmów). Użyj final dla klasy, jeśli chcesz całkowicie zabronić dziedziczenia po niej, np. dla klas narzędziowych, które nie są zaprojektowane do rozszerzania, lub gdy chcesz zachować ścisłą kontrolę nad implementacją.

Jaka jest różnica między nadpisywaniem metody a przeciążaniem metody (method overloading)?

Nadpisywanie (overriding) to definiowanie w klasie potomnej metody o tej samej sygnaturze co w klasie bazowej. Przeciążanie (overloading) to możliwość posiadania w tej samej klasie wielu metod o tej samej nazwie, ale różnych sygnaturach (np. różna liczba lub typy parametrów). PHP nie wspiera przeciążania metod w tradycyjnym sensie, jak np. Java czy C#. W PHP "przeciążanie" odnosi się raczej do metod magicznych jak __call() i __callStatic(), które pozwalają dynamicznie obsługiwać wywołania nieistniejących metod.

Czy mogę wywołać metodę z klasy "dziadka" (klasy nadrzędnej dla mojej klasy nadrzędnej) używając parent::parent::metoda()?

Nie, parent:: odnosi się tylko do bezpośredniego rodzica. Aby wywołać metodę z dalszego przodka, musiałbyś znać jego nazwę i użyć NazwaDziadka::metoda() (jeśli jest statyczna lub jeśli masz kontekst obiektu i metoda jest publiczna/chroniona i dostępna w ten sposób), lub polegać na tym, że bezpośredni rodzic poprawnie wywołuje metody swojego rodzica.

Czy właściwości mogą być final?

Nie, słowo kluczowe final w PHP nie może być stosowane do właściwości. Może być używane tylko dla klas i metod. Aby uczynić właściwość "niezmienną" po inicjalizacji, można użyć modyfikatora readonly (od PHP 8.1) dla właściwości publicznych lub polegać na hermetyzacji (prywatna właściwość i brak publicznego settera).