Lekcja 28 (OOP 8): Polimorfizm, Interfejsy (implements)

Witaj w ósmej lekcji naszego modułu o Programowaniu Obiektowym w PHP! W poprzednich lekcjach zgłębiliśmy dziedziczenie oraz klasy i metody abstrakcyjne, które pozwalają na tworzenie hierarchii klas i definiowanie wspólnych szkieletów funkcjonalności. Dzisiaj skupimy się na dwóch niezwykle ważnych koncepcjach OOP: polimorfizmie oraz interfejsach. Polimorfizm, czyli "wielopostaciowość", pozwala obiektom różnych klas reagować na to samo wywołanie metody w sposób specyficzny dla swojej klasy. Interfejsy natomiast definiują kontrakty, które klasy mogą implementować, gwarantując dostępność określonego zestawu metod, niezależnie od hierarchii dziedziczenia.

Zrozumienie polimorfizmu i interfejsów jest kluczowe dla pisania elastycznego, rozszerzalnego i łatwego w utrzymaniu kodu obiektowego. Pozwalają one na tworzenie bardziej abstrakcyjnych i luźno powiązanych komponentów systemu. Dowiemy się, jak polimorfizm manifestuje się w PHP w kontekście dziedziczenia i metod abstrakcyjnych, jak definiować i implementować interfejsy za pomocą słowa kluczowego interface i implements, oraz jakie są główne różnice i zastosowania interfejsów w porównaniu do klas abstrakcyjnych.

Czym Jest Polimorfizm?

Polimorfizm (z greckiego "poly" - wiele, "morphē" - postać, kształt) to zdolność obiektów należących do różnych klas do odpowiadania na to samo wywołanie metody (tę samą wiadomość) w sposób specyficzny dla swojej własnej klasy. Innymi słowy, ta sama operacja może zachowywać się inaczej w zależności od typu obiektu, na którym jest wykonywana.

Polimorfizm w PHP najczęściej realizowany jest poprzez:

  1. Dziedziczenie i nadpisywanie metod: Klasa potomna może nadpisać metodę odziedziczoną z klasy bazowej, dostarczając własną implementację. Gdy metoda ta jest wywoływana na obiekcie klasy potomnej, wykonuje się jej nadpisana wersja.
  2. Implementację interfejsów: Różne klasy mogą implementować ten sam interfejs, co oznacza, że każda z nich dostarczy własną implementację metod zdefiniowanych w tym interfejsie.
  3. Użycie klas abstrakcyjnych i metod abstrakcyjnych: Klasy potomne muszą zaimplementować metody abstrakcyjne zdefiniowane w klasie abstrakcyjnej, każda na swój sposób.

Główne korzyści z polimorfizmu:

Przykład Polimorfizmu z Dziedziczeniem

Kontynuując przykład z figurami geometrycznymi z poprzedniej lekcji:

<?php
abstract class FiguraGeometrycznaPolimorfizm
{
    protected string $nazwaFigury;
    public function __construct(string $nazwa) { $this->nazwaFigury = $nazwa; }
    public function getNazwa(): string { return $this->nazwaFigury; }
    abstract public function obliczPole(): float;
}

class KoloPolimorfizm extends FiguraGeometrycznaPolimorfizm
{
    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);
    }
}

class ProstokatPolimorfizm extends FiguraGeometrycznaPolimorfizm
{
    private float $bokA, $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;
    }
}

class KwadratPolimorfizm extends ProstokatPolimorfizm // Kwadrat jest szczególnym przypadkiem prostokąta
{
    public function __construct(float $bok)
    {
        parent::__construct($bok, $bok); // Wywołujemy konstruktor Prostokąta z tymi samymi bokami
        $this->nazwaFigury = "Kwadrat"; // Możemy nadpisać nazwę ustawioną przez rodzica
    }
    // Metoda obliczPole() jest dziedziczona z ProstokatPolimorfizm i działa poprawnie dla kwadratu
}

// Funkcja, która operuje na dowolnej figurze geometrycznej (polimorfizm)
function wyswietlPoleFigury(FiguraGeometrycznaPolimorfizm $figura): void
{
    echo "Figura: " . $figura->getNazwa() . "<br>";
    // Wywołanie metody obliczPole() - dzięki polimorfizmowi wykona się
    // odpowiednia implementacja dla konkretnego typu obiektu ($figura)
    echo "Pole: " . round($figura->obliczPole(), 2) . " jedn.<sup>2</sup><br>";
}

$figury = [
    new KoloPolimorfizm(5.0),
    new ProstokatPolimorfizm(4.0, 6.0),
    new KwadratPolimorfizm(3.0)
];

foreach ($figury as $figura) {
    wyswietlPoleFigury($figura);
    echo "---<br>";
}

/*
Przykładowy wynik:
Figura: Koło
Pole: 78.54 jedn.2
---
Figura: Prostokąt
Pole: 24 jedn.2
---
Figura: Kwadrat
Pole: 9 jedn.2
---
*/
?>

W tym przykładzie funkcja wyswietlPoleFigury() przyjmuje jako argument obiekt typu FiguraGeometrycznaPolimorfizm (klasa abstrakcyjna). Możemy jej przekazać obiekty klas KoloPolimorfizm, ProstokatPolimorfizm czy KwadratPolimorfizm, ponieważ wszystkie one dziedziczą po FiguraGeometrycznaPolimorfizm (spełniają relację "is-a"). Gdy wewnątrz funkcji wywołujemy $figura->obliczPole(), PHP dynamicznie określa, która wersja tej metody ma być wykonana, bazując na faktycznym typie obiektu przekazanego jako $figura. To jest właśnie polimorfizm w działaniu.

Czym Są Interfejsy?

Interfejs (interface) w PHP to konstrukcja, która definiuje zestaw publicznych metod, jakie klasa musi zaimplementować, jeśli deklaruje, że implementuje dany interfejs. Interfejs określa "co" klasa potrafi zrobić (jej kontrakt, jej API), ale nie mówi "jak" ma to zrobić (nie dostarcza implementacji metod).

Kluczowe cechy interfejsów:

Definiowanie i Implementacja Interfejsu

<?php
// Definicja interfejsu
interface Zapisywalny
{
    public const FORMAT_JSON = 'json';
    public const FORMAT_XML = 'xml';

    public function zapisz(string $sciezkaDoPliku): bool;
    public function wczytaj(string $sciezkaDoPliku): mixed; // Może zwracać różne typy
    public function getDaneDoZapisu(): array;
}

interface Logowalny
{
    public function logujWiadomosc(string $wiadomosc, string $poziom = 'info'): void;
}

// Klasa implementująca jeden interfejs
class UstawieniaAplikacji implements Zapisywalny
{
    private array $opcje = [];

    public function __construct(array $domyslneOpcje = [])
    {
        $this->opcje = $domyslneOpcje;
    }

    public function setOpcja(string $klucz, mixed $wartosc): void
    {
        $this->opcje[$klucz] = $wartosc;
    }

    public function getOpcja(string $klucz): mixed
    {
        return $this->opcje[$klucz] ?? null;
    }

    // Implementacja metod z interfejsu Zapisywalny
    public function getDaneDoZapisu(): array
    {
        return $this->opcje;
    }

    public function zapisz(string $sciezkaDoPliku): bool
    {
        $daneJson = json_encode($this->getDaneDoZapisu(), JSON_PRETTY_PRINT);
        if (file_put_contents($sciezkaDoPliku, $daneJson) !== false) {
            echo "Ustawienia zapisane do: {$sciezkaDoPliku} w formacie " . self::FORMAT_JSON . "<br>";
            return true;
        }
        echo "Błąd zapisu ustawień do: {$sciezkaDoPliku}<br>";
        return false;
    }

    public function wczytaj(string $sciezkaDoPliku): mixed
    {
        if (file_exists($sciezkaDoPliku)) {
            $daneJson = file_get_contents($sciezkaDoPliku);
            $this->opcje = json_decode($daneJson, true) ?? [];
            echo "Ustawienia wczytane z: {$sciezkaDoPliku}<br>";
            return $this->opcje;
        }
        echo "Plik {$sciezkaDoPliku} nie istnieje.<br>";
        return null;
    }
}

// Klasa implementująca wiele interfejsów
class Uzytkownik implements Zapisywalny, Logowalny
{
    public string $nazwaUzytkownika;
    private array $daneProfilu = [];

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

    public function setProfil(string $klucz, mixed $wartosc): void
    {
        $this->daneProfilu[$klucz] = $wartosc;
    }

    // Implementacja metod z Zapisywalny
    public function getDaneDoZapisu(): array
    {
        return ['nazwa' => $this->nazwaUzytkownika, 'profil' => $this->daneProfilu];
    }

    public function zapisz(string $sciezkaDoPliku): bool
    {
        // Uproszczony zapis, np. jako serializowany obiekt lub JSON
        if (file_put_contents($sciezkaDoPliku, serialize($this->getDaneDoZapisu())) !== false) {
            $this->logujWiadomosc("Profil użytkownika '{$this->nazwaUzytkownika}' zapisany.", "debug");
            return true;
        }
        return false;
    }

    public function wczytaj(string $sciezkaDoPliku): mixed
    {
        if (file_exists($sciezkaDoPliku)) {
            $dane = unserialize(file_get_contents($sciezkaDoPliku));
            if (is_array($dane)) {
                $this->nazwaUzytkownika = $dane['nazwa'] ?? 'nieznany';
                $this->daneProfilu = $dane['profil'] ?? [];
                $this->logujWiadomosc("Profil użytkownika '{$this->nazwaUzytkownika}' wczytany.");
                return $dane;
            }
        }
        return null;
    }

    // Implementacja metody z Logowalny
    public function logujWiadomosc(string $wiadomosc, string $poziom = 'info'): void
    {
        echo "[LOG - {$poziom}] ({$this->nazwaUzytkownika}): {$wiadomosc}<br>";
    }
}

// Użycie
$ustawienia = new UstawieniaAplikacji(['jezyk' => 'pl', 'motyw' => 'ciemny']);
$ustawienia->setOpcja('liczbaWynikow', 20);
$ustawienia->zapisz('ustawienia.json');
$ustawienia->wczytaj('ustawienia.json');
var_dump($ustawienia->getOpcja('motyw'));

echo "<hr>";

$user = new Uzytkownik('admin');
$user->setProfil('email', 'admin@example.com');
$user->zapisz('profil_admin.dat');
$user->logujWiadomosc("Użytkownik się zalogował.");

// Polimorfizm z interfejsami
function wykonajZapis(Zapisywalny $obiektDoZapisu, string $plik): void
{
    echo "Próba zapisu obiektu implementującego Zapisywalny...<br>";
    if ($obiektDoZapisu->zapisz($plik)) {
        echo "Zapis powiódł się dla pliku: {$plik}<br>";
    } else {
        echo "Zapis nie powiódł się dla pliku: {$plik}<br>";
    }
}

wykonajZapis($ustawienia, 'backup_ustawien.json');
wykonajZapis($user, 'backup_user_admin.dat');

?>

W tym przykładzie:

Interfejsy vs Klasy Abstrakcyjne – Kiedy Co Stosować?

Wybór między interfejsem a klasą abstrakcyjną zależy od konkretnej sytuacji i tego, co chcemy osiągnąć. Poniżej podsumowanie z poprzedniej lekcji, rozszerzone o kontekst polimorfizmu:

Często zdarza się, że klasy abstrakcyjne same implementują interfejsy, dostarczając częściową implementację metod wymaganych przez interfejs i pozostawiając resztę jako abstrakcyjne dla swoich potomków. Można też mieć hierarchię interfejsów (interfejs dziedziczący po innym interfejsie).

Polimorfizm z Interfejsami

Polimorfizm z interfejsami działa analogicznie do polimorfizmu z klasami abstrakcyjnymi. Jeśli funkcja lub metoda oczekuje obiektu implementującego dany interfejs, możemy jej przekazać obiekt dowolnej klasy, która ten interfejs implementuje. Wywołanie metody zdefiniowanej w interfejsie na takim obiekcie spowoduje wykonanie implementacji tej metody z konkretnej klasy obiektu.

Przykład z funkcją wykonajZapis(Zapisywalny $obiektDoZapisu, ...) doskonale to ilustruje. Funkcja ta nie wie, czy dostaje obiekt UstawieniaAplikacji czy Uzytkownik. Wie tylko, że obiekt ten implementuje interfejs Zapisywalny, a więc na pewno posiada metodę zapisz().

Podsumowanie Lekcji

W tej lekcji zgłębiliśmy dwie fundamentalne koncepcje OOP: polimorfizm i interfejsy. Zrozumieliśmy, że polimorfizm pozwala obiektom różnych klas reagować na to samo wywołanie metody w sposób specyficzny dla siebie, co prowadzi do bardziej elastycznego i rozszerzalnego kodu. Zobaczyliśmy, jak polimorfizm manifestuje się w PHP poprzez dziedziczenie i implementację interfejsów.

Nauczyliśmy się, jak definiować interfejsy za pomocą słowa kluczowego interface, które określają kontrakt (zestaw publicznych metod) dla klas. Klasy implementują interfejsy za pomocą implements i muszą dostarczyć implementacje wszystkich metod z interfejsu. Podkreśliliśmy, że klasa może implementować wiele interfejsów, co jest kluczowe dla modelowania złożonych zachowań.

Porównaliśmy interfejsy z klasami abstrakcyjnymi, wskazując na ich różne cele i scenariusze użycia. Interfejsy są idealne do definiowania kontraktów "can-do", podczas gdy klasy abstrakcyjne lepiej sprawdzają się do współdzielenia kodu i definiowania szkieletów dla blisko powiązanych klas w relacji "is-a".

W następnej lekcji przyjrzymy się Cechom (Traits), które oferują kolejny mechanizm reużywania kodu i horyzontalnego współdzielenia funkcjonalności w PHP.


Zadanie praktyczne

Stwórz system do odtwarzania różnych typów mediów (audio, wideo).

  1. Stwórz interfejs Odtwarzalny z następującymi metodami publicznymi:
    • odtwarzaj(): void
    • pauzuj(): void
    • stop(): void
    • getTytul(): string
  2. Stwórz klasę PlikAudio implementującą interfejs Odtwarzalny:
    • Właściwości: private string $tytulAudio, private string $artysta.
    • Konstruktor przyjmujący tytuł i artystę.
    • Zaimplementuj wszystkie metody z interfejsu Odtwarzalny. Metody odtwarzaj, pauzuj, stop powinny wyświetlać odpowiednie komunikaty (np. "Odtwarzanie audio: [tytuł] - [artysta]"). Metoda getTytul powinna zwracać $tytulAudio.
  3. Stwórz klasę PlikWideo implementującą interfejs Odtwarzalny:
    • Właściwości: private string $tytulWideo, private int $dlugoscSekundy.
    • Konstruktor przyjmujący tytuł i długość w sekundach.
    • Zaimplementuj wszystkie metody z interfejsu Odtwarzalny. Metody odtwarzaj, pauzuj, stop powinny wyświetlać odpowiednie komunikaty (np. "Odtwarzanie wideo: [tytuł] (długość: [dlugoscSekundy]s)"). Metoda getTytul powinna zwracać $tytulWideo.
  4. Stwórz funkcję uruchomOdtwarzacz(Odtwarzalny $medium): void, która przyjmuje obiekt implementujący Odtwarzalny, wyświetla jego tytuł, a następnie wywołuje na nim metody odtwarzaj(), pauzuj() i stop().
  5. Przetestuj system: utwórz obiekty PlikAudio i PlikWideo, a następnie przekaż je do funkcji uruchomOdtwarzacz().

Kliknij, aby zobaczyć przykładowe rozwiązanie
<?php
interface Odtwarzalny
{
    public function odtwarzaj(): void;
    public function pauzuj(): void;
    public function stop(): void;
    public function getTytul(): string;
}

class PlikAudio implements Odtwarzalny
{
    private string $tytulAudio;
    private string $artysta;

    public function __construct(string $tytul, string $artysta)
    {
        $this->tytulAudio = $tytul;
        $this->artysta = $artysta;
    }

    public function odtwarzaj(): void
    {
        echo "Odtwarzanie audio: " . $this->tytulAudio . " - " . $this->artysta . "<br>";
    }

    public function pauzuj(): void
    {
        echo "Pauza audio: " . $this->tytulAudio . "<br>";
    }

    public function stop(): void
    {
        echo "Stop audio: " . $this->tytulAudio . "<br>";
    }

    public function getTytul(): string
    {
        return $this->tytulAudio;
    }
}

class PlikWideo implements Odtwarzalny
{
    private string $tytulWideo;
    private int $dlugoscSekundy;

    public function __construct(string $tytul, int $dlugosc)
    {
        $this->tytulWideo = $tytul;
        $this->dlugoscSekundy = $dlugosc;
    }

    public function odtwarzaj(): void
    {
        echo "Odtwarzanie wideo: " . $this->tytulWideo . " (długość: " . $this->dlugoscSekundy . "s)<br>";
    }

    public function pauzuj(): void
    {
        echo "Pauza wideo: " . $this->tytulWideo . "<br>";
    }

    public function stop(): void
    {
        echo "Stop wideo: " . $this->tytulWideo . "<br>";
    }

    public function getTytul(): string
    {
        return $this->tytulWideo;
    }
}

function uruchomOdtwarzacz(Odtwarzalny $medium): void
{
    echo "<h3>Odtwarzacz dla: " . $medium->getTytul() . "</h3>";
    $medium->odtwarzaj();
    $medium->pauzuj();
    $medium->stop();
}

// Testowanie
$piosenka = new PlikAudio("Bohemian Rhapsody", "Queen");
$film = new PlikWideo("Incepcja", 9000); // 2.5h * 60m * 60s

uruchomOdtwarzacz($piosenka);
echo "<hr>";
uruchomOdtwarzacz($film);

/* Przykładowy wynik:
Odtwarzacz dla: Bohemian Rhapsody
Odtwarzanie audio: Bohemian Rhapsody - Queen
Pauza audio: Bohemian Rhapsody
Stop audio: Bohemian Rhapsody
---
Odtwarzacz dla: Incepcja
Odtwarzanie wideo: Incepcja (długość: 9000s)
Pauza wideo: Incepcja
Stop wideo: Incepcja
*/
?>

Zadanie do samodzielnego wykonania

Zaprojektuj system obsługi różnych metod płatności.

  1. Stwórz interfejs MetodaPlatnosci z metodami:
    • procesujPlatnosc(float $kwota): bool (zwraca true jeśli płatność się powiodła)
    • getNazwaMetody(): string
  2. Stwórz klasę PlatnoscKarta implementującą MetodaPlatnosci. Dodaj właściwości np. $numerKarty, $dataWaznosci. Metoda procesujPlatnosc powinna symulować przetwarzanie (np. wyświetlić komunikat i zwrócić true).
  3. Stwórz klasę PlatnoscPayPal implementującą MetodaPlatnosci. Dodaj właściwość np. $emailPayPal. Metoda procesujPlatnosc powinna symulować przetwarzanie.
  4. Stwórz klasę PlatnoscPrzelew implementującą MetodaPlatnosci. Dodaj właściwość np. $numerKontaBankowego. Metoda procesujPlatnosc powinna symulować przetwarzanie.
  5. Stwórz funkcję dokonajZakupu(float $kwotaDoZaplaty, MetodaPlatnosci $metoda), która wyświetla nazwę metody płatności i próbuje przetworzyć płatność.
  6. Przetestuj, tworząc różne obiekty płatności i przekazując je do funkcji dokonajZakupu.


FAQ - Polimorfizm i Interfejsy

Czy klasa może implementować interfejs i jednocześnie dziedziczyć po klasie (abstrakcyjnej lub konkretnej)?

Tak, klasa w PHP może jednocześnie dziedziczyć po jednej klasie (extends NazwaKlasyBazowej) i implementować wiele interfejsów (implements InterfejsA, InterfejsB). Słowo kluczowe extends musi wystąpić przed implements. Na przykład: class MojaSuperKlasa extends KlasaBazowa implements Interfejs1, Interfejs2 {}.

Jaka jest główna korzyść z używania typowania argumentów funkcji za pomocą interfejsów (np. function foo(NazwaInterfejsu $obj))?

Główną korzyścią jest elastyczność i luźne powiązania (loose coupling). Funkcja taka może przyjmować obiekty dowolnych klas, które implementują dany interfejs, niezależnie od ich miejsca w hierarchii dziedziczenia. Gwarantuje to, że przekazany obiekt będzie posiadał metody zdefiniowane w interfejsie, co czyni kod bardziej generycznym i łatwiejszym do rozszerzenia o nowe typy obiektów.

Czy interfejs może dziedziczyć po innym interfejsie?

Tak, interfejsy mogą dziedziczyć po jednym lub wielu innych interfejsach za pomocą słowa kluczowego extends. Interfejs potomny dziedziczy wtedy wszystkie deklaracje metod i stałych z interfejsów nadrzędnych. Na przykład: interface ZaawansowanyInterfejs extends PodstawowyInterfejs, InnyInterfejs {}.

Czy metody w interfejsie mogą mieć modyfikatory dostępu inne niż public?

Nie, wszystkie metody deklarowane w interfejsie są domyślnie publiczne. Nie można używać modyfikatorów protected ani private dla metod w interfejsie. Klasa implementująca interfejs musi zaimplementować te metody jako publiczne.

Czy interfejs może zawierać właściwości?

Nie, interfejsy nie mogą deklarować właściwości instancyjnych (zmiennych członkowskich). Mogą jedynie deklarować publiczne stałe klasowe (public const NAZWA_STALEJ = wartosc;).

Czy od PHP 8.0 interfejsy mogą mieć domyślne implementacje metod? Jak to działa?

Tak, od PHP 8.0 interfejsy mogą zawierać metody z domyślną implementacją (czyli z ciałem). Jeśli klasa implementująca taki interfejs nie dostarczy własnej implementacji dla metody z domyślnym ciałem, zostanie użyta implementacja z interfejsu. Jest to jednak mechanizm, który należy stosować ostrożnie, aby nie zacierać granicy między interfejsami a klasami abstrakcyjnymi. Głównym celem interfejsów nadal jest definiowanie kontraktu.

Kiedy polimorfizm może być trudny w debugowaniu?

Ponieważ rzeczywiste zachowanie metody zależy od dynamicznego typu obiektu, śledzenie przepływu wykonania w złożonych hierarchiach polimorficznych może być czasem trudniejsze. Dobre narzędzia deweloperskie (debuggery) i jasne nazewnictwo oraz dokumentacja mogą tu bardzo pomóc. Jednak korzyści z elastyczności zazwyczaj przewyższają te trudności.