12 Vererbung und Polymorphismus

Vererbung und Polymorphismus sind zwei zentrale Konzepte der objektorientierten Programmierung, die in PHP vollständig unterstützt werden. Diese Konzepte helfen dabei, Code effizienter zu organisieren und wiederzuverwenden, was zu saubereren und wartbareren Anwendungen führt.

12.1 Vererbung

Vererbung ermöglicht es einer Klasse (Kindklasse), Eigenschaften und Methoden einer anderen Klasse (Elternklasse) zu übernehmen. Dies fördert die Code-Wiederverwendung und etabliert eine hierarchische Beziehung zwischen Klassen.

12.1.1 Grundlegende Vererbung

In PHP wird Vererbung mit dem Schlüsselwort extends implementiert:

class Fahrzeug {
    protected string $marke;
    protected string $modell;
    protected int $baujahr;
    
    public function __construct(string $marke, string $modell, int $baujahr) {
        $this->marke = $marke;
        $this->modell = $modell;
        $this->baujahr = $baujahr;
    }
    
    public function getBeschreibung(): string {
        return "{$this->marke} {$this->modell} ({$this->baujahr})";
    }
    
    public function fahren(): string {
        return "Das Fahrzeug fährt.";
    }
}

class Auto extends Fahrzeug {
    private int $türen;
    
    public function __construct(string $marke, string $modell, int $baujahr, int $türen) {
        parent::__construct($marke, $modell, $baujahr);
        $this->türen = $türen;
    }
    
    public function getBeschreibung(): string {
        return parent::getBeschreibung() . " mit {$this->türen} Türen";
    }
}

// Verwendung
$meinAuto = new Auto("VW", "Golf", 2022, 5);
echo $meinAuto->getBeschreibung(); // Ausgabe: VW Golf (2022) mit 5 Türen
echo $meinAuto->fahren(); // Ausgabe: Das Fahrzeug fährt.

In diesem Beispiel: - Auto erbt alle öffentlichen und geschützten Eigenschaften und Methoden von Fahrzeug - Auto erweitert die geerbte Funktionalität um eine zusätzliche Eigenschaft $türen - Auto überschreibt die Methode getBeschreibung(), nutzt aber die Elternimplementierung mit parent:: - Auto erbt die Methode fahren() unverändert

12.1.2 Der parent-Operator

Der parent::-Operator ermöglicht den Zugriff auf überschriebene Eigenschaften und Methoden der Elternklasse:

class Motorrad extends Fahrzeug {
    private bool $hatBeiwagen;
    
    public function __construct(string $marke, string $modell, int $baujahr, bool $hatBeiwagen) {
        parent::__construct($marke, $modell, $baujahr);
        $this->hatBeiwagen = $hatBeiwagen;
    }
    
    public function fahren(): string {
        return parent::fahren() . " Aber vorsichtig, es ist ein Motorrad!";
    }
}

12.1.3 Zugriffsmodifikatoren bei Vererbung

Bei der Vererbung gelten folgende Regeln für Zugriffsmodifikatoren:

class Basis {
    public $öffentlich = "Zugriff von überall";
    protected $geschützt = "Zugriff in Basis und abgeleiteten Klassen";
    private $privat = "Nur Zugriff in Basis";
    
    public function testZugriff() {
        echo $this->öffentlich;  // Erlaubt
        echo $this->geschützt;   // Erlaubt
        echo $this->privat;      // Erlaubt
    }
}

class Abgeleitet extends Basis {
    public function testZugriff() {
        echo $this->öffentlich;  // Erlaubt
        echo $this->geschützt;   // Erlaubt
        echo $this->privat;      // Fehler! Kein Zugriff auf private Elemente der Elternklasse
    }
}

12.1.4 Verhindern von Vererbung und Überschreibung

Seit PHP 5.5 kann die Vererbung mit dem Schlüsselwort final verhindert werden:

// Diese Klasse kann nicht erweitert werden
final class EndgültigeKlasse {
    // Implementierung...
}

// Fehler: Class KannNichtErweitern may not inherit from final class (EndgültigeKlasse)
class KannNichtErweitern extends EndgültigeKlasse {
    // ...
}

Das final-Schlüsselwort kann auch auf Methoden angewendet werden, um zu verhindern, dass sie in abgeleiteten Klassen überschrieben werden:

class Basis {
    final public function unüberschreibbareMethode() {
        // Implementierung...
    }
}

class Abgeleitet extends Basis {
    // Fehler: Cannot override final method Basis::unüberschreibbareMethode()
    public function unüberschreibbareMethode() {
        // ...
    }
}

12.2 Polymorphismus

Polymorphismus erlaubt es, Objekte verschiedener Klassen durch eine gemeinsame Schnittstelle zu behandeln. In PHP wird dies hauptsächlich durch Methodenüberschreibung und Typdeklarationen erreicht.

12.2.1 Methodenüberschreibung

Eine abgeleitete Klasse kann eine Methode der Elternklasse überschreiben, um ihr eigenes Verhalten zu implementieren:

class Form {
    public function berechneFlaeche(): float {
        return 0.0;
    }
    
    public function beschreibung(): string {
        return "Ich bin eine Form mit einer Fläche von " . $this->berechneFlaeche() . " Quadrateinheiten.";
    }
}

class Kreis extends Form {
    private float $radius;
    
    public function __construct(float $radius) {
        $this->radius = $radius;
    }
    
    public function berechneFlaeche(): float {
        return pi() * $this->radius * $this->radius;
    }
}

class Rechteck extends Form {
    private float $breite;
    private float $höhe;
    
    public function __construct(float $breite, float $höhe) {
        $this->breite = $breite;
        $this->höhe = $höhe;
    }
    
    public function berechneFlaeche(): float {
        return $this->breite * $this->höhe;
    }
}

12.2.2 Verwendung von Polymorphismus

Der wahre Nutzen des Polymorphismus wird deutlich, wenn wir Objekte verschiedener Klassen einheitlich behandeln:

// Array von verschiedenen Formen
$formen = [
    new Kreis(5),
    new Rechteck(4, 6),
    new Kreis(3)
];

// Einheitlicher Umgang mit allen Formen
foreach ($formen as $form) {
    echo $form->beschreibung() . PHP_EOL;
}

// Berechnung der Gesamtfläche
$gesamtFlaeche = array_sum(array_map(function($form) {
    return $form->berechneFlaeche();
}, $formen));

echo "Gesamtfläche: " . $gesamtFlaeche;

12.2.3 Typ-Deklarationen und Polymorphismus

Seit PHP 7 können wir Typ-Deklarationen verwenden, um sicherzustellen, dass eine Funktion oder Methode ein Objekt eines bestimmten Typs (oder einer abgeleiteten Klasse) erwartet:

function druckeFormInfo(Form $form): void {
    echo "Form-Info: " . $form->beschreibung();
}

// Funktioniert mit jeder Klasse, die von Form erbt
druckeFormInfo(new Kreis(7));       // Gültig
druckeFormInfo(new Rechteck(2, 3)); // Gültig
druckeFormInfo(new stdClass());     // Fehler! stdClass ist keine Form

12.3 Praktisches Beispiel: Ein Zahlungssystem

Um die Konzepte der Vererbung und des Polymorphismus in einem realitätsnahen Kontext zu demonstrieren, betrachten wir ein einfaches Zahlungssystem:

abstract class Zahlungsmethode {
    protected float $betrag;
    
    public function __construct(float $betrag) {
        $this->betrag = $betrag;
    }
    
    // Abstrakte Methode, die von allen konkreten Zahlungsmethoden implementiert werden muss
    abstract public function prozessieren(): bool;
    
    public function getBetrag(): float {
        return $this->betrag;
    }
}

class Kreditkarte extends Zahlungsmethode {
    private string $kartenNummer;
    private string $ablaufDatum;
    private string $ccv;
    
    public function __construct(float $betrag, string $kartenNummer, string $ablaufDatum, string $ccv) {
        parent::__construct($betrag);
        $this->kartenNummer = $kartenNummer;
        $this->ablaufDatum = $ablaufDatum;
        $this->ccv = $ccv;
    }
    
    public function prozessieren(): bool {
        // In einer realen Anwendung würde hier die Kreditkartenzahlung verarbeitet
        echo "Verarbeite Kreditkartenzahlung über {$this->betrag}€...";
        return true;
    }
}

class PayPal extends Zahlungsmethode {
    private string $email;
    
    public function __construct(float $betrag, string $email) {
        parent::__construct($betrag);
        $this->email = $email;
    }
    
    public function prozessieren(): bool {
        // In einer realen Anwendung würde hier die PayPal-Zahlung verarbeitet
        echo "Verarbeite PayPal-Zahlung über {$this->betrag}€ für {$this->email}...";
        return true;
    }
}

class Banküberweisung extends Zahlungsmethode {
    private string $iban;
    private string $bic;
    
    public function __construct(float $betrag, string $iban, string $bic) {
        parent::__construct($betrag);
        $this->iban = $iban;
        $this->bic = $bic;
    }
    
    public function prozessieren(): bool {
        // In einer realen Anwendung würde hier die Banküberweisung verarbeitet
        echo "Verarbeite Banküberweisung über {$this->betrag}€ an {$this->iban}...";
        return true;
    }
}

// Verwenden des Zahlungssystems (polymorphischer Ansatz)
function zahlungDurchführen(Zahlungsmethode $zahlungsmethode): void {
    echo "Zahlung über " . $zahlungsmethode->getBetrag() . "€ wird verarbeitet...\n";
    
    if ($zahlungsmethode->prozessieren()) {
        echo "Zahlung erfolgreich abgeschlossen!\n";
    } else {
        echo "Zahlung fehlgeschlagen!\n";
    }
}

// Verschiedene Zahlungsmethoden testen
$kreditkarte = new Kreditkarte(99.99, "1234-5678-9012-3456", "12/25", "123");
$paypal = new PayPal(49.99, "kunde@example.com");
$banküberweisung = new Banküberweisung(199.99, "DE89370400440532013000", "COBADEFFXXX");

zahlungDurchführen($kreditkarte);
zahlungDurchführen($paypal);
zahlungDurchführen($banküberweisung);

In diesem Beispiel: - Zahlungsmethode ist eine abstrakte Basisklasse, die gemeinsame Eigenschaften und eine Schnittstellendefinition bereitstellt - Jede konkrete Zahlungsmethode erweitert die Basisklasse und implementiert ihre eigene Version von prozessieren() - Die Funktion zahlungDurchführen() arbeitet mit jeder Art von Zahlungsmethode, ohne deren spezifischen Typ zu kennen

12.4 Vorteile von Vererbung und Polymorphismus

  1. Code-Wiederverwendung: Gemeinsamer Code muss nur einmal in der Basisklasse geschrieben werden
  2. Wartbarkeit: Änderungen an gemeinsamer Funktionalität müssen nur an einer Stelle vorgenommen werden
  3. Erweiterbarkeit: Neue Unterklassen können hinzugefügt werden, ohne bestehenden Code zu ändern
  4. Flexibilität: Polymorphe Methoden können verschiedene Implementierungen haben, aber über eine gemeinsame Schnittstelle angesprochen werden

12.5 Hinweise und Best Practices

  1. Vererbungshierarchie flach halten: Tiefe Vererbungshierarchien werden schnell komplex und schwer zu verwalten. Als Faustregel gilt, nicht mehr als 2-3 Ebenen zu verwenden.

  2. Komposition vs. Vererbung: Manchmal ist Komposition (das Einbetten von Objekten in andere Objekte) besser als Vererbung. Die Regel “Bevorzuge Komposition vor Vererbung” ist oft sinnvoll zu beachten.

  3. Liskovsches Substitutionsprinzip: Objekte einer abgeleiteten Klasse sollten ohne Probleme für Objekte der Basisklasse eingesetzt werden können, ohne dass die Anwendung davon betroffen ist. Dies ist ein grundlegendes Prinzip für guten objektorientierten Code.

  4. Abstrakte Klassen für gemeinsame Implementierungen: Wenn mehrere Klassen gemeinsame Implementierungen haben, aber nicht direkt instanziiert werden sollten, sind abstrakte Klassen das richtige Werkzeug.