Abschnitt 13 · Lektion 3
Vererbung
In der letzten Lektion hat jede Klasse für sich gestanden. Jetzt kommen zwei dazu, die einander ähnlich sind, und damit die Frage, was man mit der Ähnlichkeit anfängt.
Das Problem: dieselbe Methode, zweimal aufgeschrieben
<?php
class Brief
{
public function __construct(
private readonly string $name,
private readonly int $gramm,
) {
}
public function preis(): float
{
return 0.85;
}
public function zeile(): string
{
return $this->name . ": " . number_format($this->preis(), 2, ",", ".");
}
}
class Paket
{
public function __construct(
private readonly string $name,
private readonly int $gramm,
) {
}
public function preis(): float
{
return 2.99 + $this->gramm / 1000 * 1.25;
}
// Wortgleich mit der Fassung oben. Wer hier etwas aendert,
// muss daran denken, es zweimal zu tun.
public function zeile(): string
{
return $this->name . ": " . number_format($this->preis(), 2, ",", ".");
}
}
echo (new Brief("Brief", 20))->zeile(), "\n";
echo (new Paket("Paket", 2000))->zeile(), "\n"; Brief und Paket unterscheiden sich in genau einer Methode. Der Konstruktor ist gleich, zeile()
ist wortgleich, und beides steht zweimal da.
Das ist nicht nur unschön. Es ist eine Verabredung, die niemand einhalten muss: Wer morgen das Format der Zeile ändert, ändert es an einer Stelle und übersieht die andere. Das Programm läuft weiter und gibt zwei verschiedene Formate aus.
extends zieht das Gemeinsame nach oben
<?php
abstract class Sendung
{
public function __construct(
private readonly string $name,
protected readonly int $gramm,
) {
}
abstract public function preis(): float;
public function zeile(): string
{
return $this->name . ": " . number_format($this->preis(), 2, ",", ".");
}
}
class Brief extends Sendung
{
public function preis(): float
{
return 0.85;
}
}
class Paket extends Sendung
{
public function preis(): float
{
return 2.99 + $this->gramm / 1000 * 1.25;
}
}
echo (new Brief("Brief", 20))->zeile(), "\n";
echo (new Paket("Paket", 2000))->zeile(), "\n"; class Brief extends Sendung heißt: Brief hat alles, was Sendung hat, und darf etwas dazutun.
Der Konstruktor steht nur noch einmal da, zeile() ebenfalls, und die Unterklassen bestehen aus
genau der Methode, in der sie sich wirklich unterscheiden.
Wichtig ist dabei, was geerbt wird. Methoden und Eigenschaften wandern nach unten, aber die Sichtbarkeit gilt weiter:
publicundprotectedsind in der Unterklasse benutzbar.privateist es nicht. Eine Eigenschaft, die inSendungprivateist, existiert fürBriefnicht. Ein Zugriff darauf ist keine Fehlermeldung über Sichtbarkeit, sondern eine ganz gewöhnlicheUndefined property-Warnung, denn aus Sicht der Unterklasse gibt es sie schlicht nicht.
Genau dafür ist protected da, und das ist auch schon seine ganze Aufgabe: „diese Klasse und alles,
was von ihr erbt”. Im Beispiel ist $gramm deshalb protected, denn Paket::preis() rechnet
damit. $name bleibt private, weil ihn nur zeile() in der Oberklasse braucht.
Im Reiter Debug ist beides zu sehen. Nach Aufzeichnen stehen in zeile() $this->name und
$this->gramm in der Liste; einen Schritt weiter, in Brief::preis(), steht dort nur noch
$this->gramm. Der Name ist für die Unterklasse nicht etwa unsichtbar, er ist nicht da. Und die
Zeile mit return $this->name taucht zweimal auf, einmal mit "Brief" und einmal mit "Paket";
dazwischen springt die Liste einmal in das preis() von Brief und einmal in das von Paket.
Eine Methode, zwei Ergebnisse.
Überschreiben, und parent:: für den Fall dazwischen
<?php
class Sendung
{
public function __construct(protected readonly int $gramm)
{
}
public function preis(): float
{
return 2.99 + $this->gramm / 1000 * 1.25;
}
}
class Express extends Sendung
{
public function __construct(int $gramm, private readonly bool $samstag)
{
parent::__construct($gramm);
}
// Nicht ersetzen, sondern draufrechnen.
public function preis(): float
{
return parent::preis() + ($this->samstag ? 4.00 : 2.00);
}
}
$normal = new Sendung(2000);
$express = new Express(2000, true);
echo "Normal: ", number_format($normal->preis(), 2, ",", "."), "\n";
echo "Express: ", number_format($express->preis(), 2, ",", "."), "\n"; Schreibt eine Unterklasse eine Methode noch einmal, die es oben schon gibt, gilt ihre Fassung. Das ist Überschreiben, und meistens will man genau das.
Manchmal will man aber nicht ersetzen, sondern ergänzen: der Expressversand kostet das Normale
plus einen Zuschlag. Dafür gibt es parent::, mit dem du die Fassung der Oberklasse aufrufst
und ihr Ergebnis weiterverwendest. Dasselbe gilt für den Konstruktor: Wenn eine Unterklasse einen
eigenen braucht, ruft sie mit parent::__construct(...) den von oben auf. Vergisst sie das, wird
der Konstruktor der Oberklasse gar nicht ausgeführt. Die geerbten Eigenschaften haben dann
keinen Wert, und der erste Zugriff darauf bricht mit
Typed property Sendung::$gramm must not be accessed before initialization ab, also mit demselben
Fehler wie das halbfertige Objekt aus Lektion 13.1.
Der Reiter Debug zeigt genau diesen Moment: An der Zeile mit parent::__construct($gramm) steht
in der Liste schon $this->samstag, aber noch kein $this->gramm. Erst beim nächsten Schritt in
einer Methode von Express, in preis(), ist es da.
abstract: der Bauplan, von dem es kein Haus gibt
<?php
abstract class Sendung
{
// Kein Koerper, nur ein Semikolon: jede Variante muss das
// selbst wissen.
abstract public function preis(): float;
}
class Brief extends Sendung
{
public function preis(): float
{
return 0.85;
}
}
echo "Brief: ", number_format((new Brief())->preis(), 2, ",", "."), "\n";
try {
new Sendung();
} catch (Error $fehler) {
echo "Abgelehnt: ", $fehler->getMessage(), "\n";
} abstract class Sendung heißt: Diese Klasse ist nur zum Erben da. new Sendung() ist ein Error
mit der Meldung Cannot instantiate abstract class Sendung, und das ist richtig so, denn eine
„Sendung” ohne Angabe, welche, hat keinen Preis.
abstract public function preis(): float; ist die zweite Hälfte davon: eine Methode ohne Körper,
nur mit Semikolon. Sie sagt „das muss jede Variante selbst wissen”, und PHP setzt es durch: Eine
Unterklasse, die sie nicht schreibt, lässt sich nicht einmal einlesen.
Damit ist die abstrakte Klasse mehr als ein Sammelbecken. Sie ist eine Liste aus zwei Teilen: Das hier gilt für alle, und das hier muss jeder selbst beantworten.
Und was daraus wird, wenn es eine Seite werden soll
<?php
abstract class Sendung
{
public function __construct(
protected readonly string $name,
protected readonly int $gramm,
) {
}
abstract public function preis(): float;
// Eine Methode in der Oberklasse, zwei Ergebnisse: preis()
// kommt jeweils aus der Unterklasse.
public function reihe(): string
{
return "<tr><td>" . htmlspecialchars($this->name, ENT_QUOTES, "UTF-8")
. "</td><td>" . $this->gramm . " g</td><td>"
. number_format($this->preis(), 2, ",", ".") . " EUR</td></tr>";
}
}
class Brief extends Sendung
{
public function preis(): float
{
return 0.85;
}
}
class Paket extends Sendung
{
public function preis(): float
{
return 2.99 + $this->gramm / 1000 * 1.25;
}
}
// Das style-Element muss mit in die Ausgabe: der Browser bekommt
// einen Abzug ohne Adresse, eine eigene CSS-Datei laedt dort nicht.
echo "<style>body{font-family:system-ui;margin:1rem}";
echo "table{border-collapse:collapse}";
echo "td,th{border:1px solid #999;padding:.35rem .6rem;text-align:left}</style>\n";
echo "<h1>Porto</h1>\n";
echo "<table><tr><th>Sendung</th><th>Gewicht</th><th>Preis</th></tr>\n";
foreach ([new Brief("Brief", 20), new Paket("Paket", 2000)] as $sendung) {
echo $sendung->reihe(), "\n";
}
echo "</table>\n"; Dieses Beispiel gibt HTML aus, und deshalb läuft ein Server und im Reiter Browser steht eine Seite. Es ist das einzige in diesem Abschnitt, alle anderen spielen auf der Kommandozeile.
Sieh dir an, was hier eigentlich passiert: reihe() steht einmal in der Oberklasse und liefert
trotzdem für Brief und Paket verschiedene Zeilen, weil $this->preis() jeweils die Fassung der
Unterklasse nimmt. Das ist der praktische Gewinn der ganzen Lektion, und er steht in einer einzigen Methode.
Wann Vererbung passt, und wann nicht
Vererbung passt, wenn zwei Klassen dieselbe Sache in Varianten sind. Ein Brief ist eine Sendung, ein Paket ist eine Sendung. Die Probe dafür ist der Satz „ein X ist ein Y”: Wenn er sich falsch anhört, ist es keine Vererbung.
Sie passt nicht, wenn zwei Dinge sich nur ähneln oder wenn eine Klasse etwas hat, statt etwas zu sein. Eine Bestellung hat eine Adresse, sie ist keine. Solche Fälle löst man, indem man das Objekt hineingibt, nicht indem man erbt.
Und noch etwas: Der Kurs bleibt bei einer Ebene. Drei oder vier Stufen sind schnell gebaut, und danach muss man beim Lesen jeder Methode raten, aus welcher der vier Klassen sie eigentlich kommt. Wo es eng wird, ist meistens nicht ein tieferer Baum die Antwort, sondern die nächste Lektion.
Zum Mitnehmen
Zwei Klassen, die zu vier Fünfteln dasselbe tun, sind zwei Stellen, an denen dieselbe Änderung nötig wird. Vererbung zieht das Gemeinsame nach oben. Sie ist dabei ein schärferes Werkzeug, als sie aussieht, und dieser Artikel sagt auch, wann man sie stehen lässt.
Jetzt du
Basis Konto, kostenlosZu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.
Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.
In diesem Kurs läuft dein Code auf einem Server. Dafür hat das Basis Konto 1 Stunde im Monat, mehr Zeit gibt es mit dem Premium Konto.
Was in dieser Lektion steckt
-
Artikel mit 5 Beispielen zum Ausprobieren
Steht hier, ohne Konto lesbar.
-
Aufgabe, dein Code läuft auf einem Server
Öffnet sich mit dem Basis Konto.