die richtige Wahl im Entwurf treffen
Eine abstrakte Klasse und ein Interface lösen auf den ersten Blick ein ähnliches Problem, nämlich einen Vertrag festzulegen, dem konkrete Klassen folgen müssen. Der entscheidende Unterschied liegt darin, was jede der beiden Konstruktionen tatsächlich mitbringt: Ein Interface ist ein reiner Vertrag ohne jede Implementierung, eine abstrakte Klasse dagegen kann Konstruktoren, geteilten Zustand und konkrete Methoden enthalten, die alle Unterklassen automatisch erben. Dieser Beitrag zeigt an einem durchgehenden Report-Generator-Beispiel, wann welches Werkzeug die richtige Wahl ist, warum die Single-Inheritance-Beschränkung von PHP diese Entscheidung entscheidend prägt und wie beide Konzepte in einer einzigen Klassenhierarchie sauber zusammenspielen.
Inhaltsverzeichnis
- 1. Der Kernunterschied: Vertrag vs. Teilimplementierung
- 2. Konstruktoren und geteilter Zustand in abstrakten Klassen
- 3. Das Template-Method-Pattern als Paradebeispiel
- 4. Die Single-Inheritance-Beschränkung
- 5. Interfaces als reine Verträge im Detail
- 6. Beide bewusst kombinieren
- 7. Häufige Fehler im Entwurf
- 8. Entscheidungsheuristik für die Praxis
- 9. Abstrakte Klasse vs. Interface im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Der Kernunterschied: Vertrag vs. Teilimplementierung
Ein Interface in PHP definiert ausschließlich, welche Methoden eine Klasse anbieten muss, ohne dass auch nur eine Zeile Implementierung, geschweige denn eine Property oder ein Konstruktor, Teil des Interfaces sein dürfte. Eine abstrakte Klasse dagegen darf beides gleichzeitig tun: Sie kann konkrete Methoden mit vollständigem Code bereitstellen und gleichzeitig einzelne Methoden als abstract markieren, die jede konkrete Unterklasse selbst implementieren muss. Diese Kombination aus fertigem und noch offenem Code ist genau der Punkt, an dem sich eine abstrakte Klasse fundamental von einem Interface unterscheidet.
Am deutlichsten wird der Unterschied, wenn mehrere Unterklassen dieselbe Teillogik brauchen. Würde man dieselbe Aufgabe allein mit einem Interface lösen, müsste jede einzelne implementierende Klasse dieselbe Logik erneut schreiben, weil ein Interface strukturell keinen Platz für gemeinsamen Code bietet. Eine abstrakte Klasse löst genau dieses Problem, indem sie den gemeinsamen Teil einmal zentral implementiert und nur den variablen Teil an die Unterklassen delegiert.
<?php
declare(strict_types=1);
namespace Reporting;
// Abstract class: shared constant logic PLUS an open contract in one construct
abstract class AbstractReportGenerator
{
public function renderHeader(string $title): string
{
return sprintf("=== %s ===\n", strtoupper($title));
}
// Every concrete subclass must supply its own data source
abstract public function fetchData(): array;
}
// The equivalent interface-only version would lose renderHeader() entirely:
// every implementing class would have to duplicate that formatting logic.
interface ReportGeneratorInterface
{
public function fetchData(): array;
}
In diesem Gegenbeispiel zeigt sich der Preis eines reinen Interfaces: renderHeader() müsste in jeder einzelnen implementierenden Klasse separat geschrieben werden, mit dem Risiko, dass die Formatierung irgendwann zwischen den Klassen auseinanderdriftet. Die abstrakte Klasse verhindert genau das, weil der gemeinsame Code an genau einer Stelle existiert und von dort aus vererbt wird.
2. Konstruktoren und geteilter Zustand in abstrakten Klassen
Ein struktureller Vorteil einer abstrakten Klasse gegenüber einem Interface ist die Fähigkeit, einen eigenen Konstruktor zu deklarieren. Dieser Konstruktor kann mit Constructor Property Promotion und readonly-Properties genau die Invarianten erzwingen, die jede Unterklasse erfüllen muss, ohne dass die Unterklasse selbst etwas dafür tun muss außer parent::__construct() aufzurufen. Ein Interface kann das strukturell nicht leisten, weil es überhaupt keine Konstruktorsignatur vorschreiben kann, geschweige denn Konstruktorlogik ausführen.
Das folgende Beispiel erweitert den Report-Generator um einen Konstruktor, der einen Zeitstempel und einen Klassennamen als geteilten Zustand festlegt, den alle konkreten Reports gleichermaßen nutzen können, ohne ihn selbst neu zu berechnen.
<?php
declare(strict_types=1);
namespace Reporting;
abstract class AbstractReportGenerator
{
public readonly \DateTimeImmutable $generatedAt;
public function __construct(
private readonly string $reportName,
) {
// Shared invariant: every report knows exactly when it was created
$this->generatedAt = new \DateTimeImmutable();
}
public function renderHeader(): string
{
return sprintf(
"=== %s (generated %s) ===\n",
strtoupper($this->reportName),
$this->generatedAt->format('Y-m-d H:i:s')
);
}
abstract public function fetchData(): array;
}
final class SalesReportGenerator extends AbstractReportGenerator
{
public function __construct(private readonly \PDO $db)
{
parent::__construct('Sales Report'); // must call parent constructor
}
public function fetchData(): array
{
return $this->db->query('SELECT * FROM sales')->fetchAll();
}
}
Jede Unterklasse von AbstractReportGenerator erhält generatedAt automatisch, ohne die Logik selbst zu duplizieren, muss aber den Konstruktoraufruf explizit weiterreichen. Genau diese Kombination aus erzwungenem Konstruktoraufruf und geteiltem Zustand ist mit einem reinen Interface strukturell nicht erreichbar, weil ein Interface keinen Platz für Property-Deklarationen oder Konstruktorlogik bietet.
3. Das Template-Method-Pattern als Paradebeispiel
Der klassische Lehrbuchfall für eine abstrakte Klasse ist das Template-Method-Pattern. Dabei definiert die Basisklasse eine als final markierte Methode, die den gesamten Ablauf eines Algorithmus in fester Reihenfolge festlegt, ruft dabei aber an einzelnen Stellen abstract deklarierte Hook-Methoden auf, die jede Unterklasse individuell füllen muss. Die Struktur des Ablaufs bleibt für alle Unterklassen garantiert identisch, während der fachliche Inhalt an genau den vorgesehenen Stellen variiert.
Diese Garantie ist der eigentliche Wert des Patterns: Kein Unterklassen-Autor kann versehentlich vergessen, den Header vor den Daten zu rendern, weil die Reihenfolge in der final-Methode fest verdrahtet ist und von keiner Unterklasse überschrieben werden kann.
<?php
declare(strict_types=1);
namespace Reporting;
abstract class AbstractReportGenerator
{
// final: the overall algorithm structure is fixed for every subclass
final public function generate(): string
{
$output = $this->renderHeader();
foreach ($this->fetchData() as $row) {
$output .= $this->formatRow($row);
}
return $output;
}
abstract protected function renderHeader(): string;
abstract protected function fetchData(): array;
abstract protected function formatRow(array $row): string;
}
final class InventoryReportGenerator extends AbstractReportGenerator
{
public function __construct(private readonly \PDO $db)
{
}
protected function renderHeader(): string
{
return "=== INVENTORY REPORT ===\n";
}
protected function fetchData(): array
{
return $this->db->query('SELECT sku, qty FROM inventory')->fetchAll();
}
protected function formatRow(array $row): string
{
return sprintf("%s: %d units\n", $row['sku'], $row['qty']);
}
}
InventoryReportGenerator muss nur die drei fachlich variablen Methoden implementieren, während generate() selbst niemals überschrieben werden kann, weil sie final ist. Ein Interface könnte den gleichen Vertrag für fetchData() und formatRow() zwar auch vorschreiben, aber die feste Ablaufreihenfolge in generate() müsste dann in jeder implementierenden Klasse eigenständig und potenziell inkonsistent nachgebaut werden.
4. Die Single-Inheritance-Beschränkung
PHP erlaubt einer Klasse, genau eine abstrakte Klasse zu erweitern, aber beliebig viele Interfaces gleichzeitig zu implementieren. Diese Asymmetrie ist keine willkürliche Sprachentscheidung, sondern eine bewusste Vermeidung des klassischen Diamond-Problems, das in Sprachen mit echter Mehrfachvererbung von Implementierung auftreten kann: Wenn zwei Basisklassen dieselbe Methode unterschiedlich implementieren, wäre nicht eindeutig, welche Version eine gemeinsame Unterklasse erben sollte.
Diese Beschränkung hat direkte Konsequenzen für den Entwurf. Wer versucht, zwei völlig unabhängige Verhaltensbündel über zwei abstrakte Basisklassen gleichzeitig in eine Klasse zu mischen, stößt sofort an die Sprachgrenze. Die praktische Konsequenz: Sobald mehr als eine Verhaltensdimension gebraucht wird, führt der Weg über Interfaces plus Komposition, nicht über eine zweite abstrakte Basisklasse. Eine tiefe Kette abstrakter Klassen, die versucht, immer mehr Verhalten in einer einzigen Vererbungslinie zu bündeln, wird dadurch mit wachsender Größe zunehmend unflexibel.
5. Interfaces als reine Verträge im Detail
Ein Interface darf Konstanten deklarieren, aber keine Properties und keine Methodenimplementierung. Eine Klasse darf beliebig viele Interfaces gleichzeitig implementieren, solange sie für jede einzelne Methode aus jedem Interface eine konkrete Implementierung liefert. Genau diese Eigenschaft macht Interfaces zum richtigen Werkzeug, wenn mehrere, fachlich völlig unabhängige Typen denselben Vertrag erfüllen sollen, ohne dass sie einen gemeinsamen Vorfahren teilen müssten.
Für Tests und lose Kopplung ist genau das entscheidend: Ein Interface wie ReportGeneratorInterface lässt sich in einem Unit-Test durch ein Test-Double ersetzen, ohne dass der Test irgendetwas über eine konkrete Klassenhierarchie wissen müsste. Eine abstrakte Klasse als Abhängigkeit vorauszusetzen würde dagegen bedeuten, dass ein Test-Double dieselbe Vererbungslinie erben müsste, was die Kopplung an die konkrete Implementierung unnötig verstärkt.
<?php
declare(strict_types=1);
namespace Reporting;
interface ExportableInterface
{
public function toCsv(): string;
}
interface SchedulableInterface
{
public function nextRunAt(): \DateTimeImmutable;
}
// One class can implement as many independent interfaces as needed
final class SalesReportGenerator extends AbstractReportGenerator implements
ExportableInterface,
SchedulableInterface
{
public function toCsv(): string
{
// ...
return '';
}
public function nextRunAt(): \DateTimeImmutable
{
return new \DateTimeImmutable('tomorrow 06:00');
}
}
6. Beide bewusst kombinieren
In realen Codebasen ist die Wahl selten entweder-oder. Eine Klasse kann gleichzeitig eine abstrakte Klasse erweitern, die einen Teil der Arbeit übernimmt, und ein Interface implementieren, das den nach außen sichtbaren Vertrag beschreibt. Die abstrakte Klasse kann dabei sogar einen Teil der Interface-Methoden bereits konkret vorimplementieren und nur die restlichen als abstract offenlassen, sodass jede Unterklasse nur noch den fachlich spezifischen Teil ausfüllen muss.
<?php
declare(strict_types=1);
namespace Reporting;
interface ReportGeneratorInterface
{
public function generate(): string;
public function fetchData(): array;
}
// Abstract class implements PART of the interface, leaves the rest open
abstract class AbstractReportGenerator implements ReportGeneratorInterface
{
final public function generate(): string
{
$output = "=== Report ===\n";
foreach ($this->fetchData() as $row) {
$output .= json_encode($row) . "\n";
}
return $output;
}
// fetchData() remains abstract, ReportGeneratorInterface is not fully satisfied here
abstract public function fetchData(): array;
}
final class UserReportGenerator extends AbstractReportGenerator
{
public function fetchData(): array
{
return [['id' => 1, 'name' => 'Ada']];
}
}
// Both relationships hold at the same time
$report = new UserReportGenerator();
var_dump($report instanceof ReportGeneratorInterface); // true
var_dump($report instanceof AbstractReportGenerator); // true
Diese Drei-Wege-Beziehung, Klasse erweitert abstrakte Klasse und implementiert damit indirekt ein Interface, kombiniert die Vorteile beider Konzepte: die geteilte Implementierung aus der abstrakten Klasse und die lose Kopplung über den Interface-Typ, gegen den anderer Code programmieren kann, ohne die konkrete Vererbungslinie zu kennen.
7. Häufige Fehler im Entwurf
Ein wiederkehrender Fehler ist die Verwendung einer abstrakten Klasse allein als Behälter für Konstanten, ohne dass sie tatsächlich geteilte Implementierung oder Zustand mitbringt. In diesem Fall wäre ein Interface mit Konstanten oder, seit PHP 8.1, ein enum die passendere Wahl, weil beide keine unnötige Vererbungsbeziehung erzwingen, die später zu unerwarteten Kopplungen führen kann.
Der umgekehrte Fehler tritt auf, wenn ein Interface dort verwendet wird, wo tatsächlich geteilte Implementierung nötig wäre. Das Ergebnis ist fast immer identischer Code, der in jeder implementierenden Klasse separat existiert und bei einer Änderung an mehreren Stellen gleichzeitig gepflegt werden muss, mit dem entsprechenden Risiko, dass eine Stelle vergessen wird. Ein dritter, subtilerer Fehler ist das "Fragile Base Class"-Problem: Je tiefer eine Kette abstrakter Klassen wird, desto größer die Gefahr, dass eine Änderung an einer gemeinsamen Methode das Verhalten aller Unterklassen gleichzeitig und unbeabsichtigt verändert, weit entfernt von der Stelle, an der die Änderung eigentlich gedacht war.
8. Entscheidungsheuristik für die Praxis
Eine praktikable Entscheidungshilfe lässt sich auf drei Fragen reduzieren. Erstens: Muss tatsächlich konkreter Code oder Zustand geteilt werden, nicht nur eine Methodensignatur? Falls ja, spricht das für eine abstrakte Klasse. Zweitens: Müssen mehrere, fachlich völlig unabhängige Typen denselben Vertrag erfüllen, ohne einen gemeinsamen Vorfahren zu teilen? Falls ja, spricht das eindeutig für ein Interface. Drittens: Wird das gemeinsame Verhalten sich künftig unabhängig von den einzelnen Unterklassen weiterentwickeln müssen? Falls ja, ist oft Komposition, ein Objekt hält eine Referenz auf ein anderes Objekt statt es zu erben, die robustere Alternative zu einer tiefen Vererbungshierarchie.
Komposition löst dabei ein Problem, das weder Interfaces noch abstrakte Klassen strukturell lösen können: die Möglichkeit, Verhalten zur Laufzeit auszutauschen, ohne die Klassenhierarchie selbst zu verändern.
<?php
declare(strict_types=1);
namespace Reporting;
interface FormatterInterface
{
public function format(array $row): string;
}
final class JsonFormatter implements FormatterInterface
{
public function format(array $row): string
{
return json_encode($row) . "\n";
}
}
// Composition: the generator HOLDS a formatter instead of inheriting one
final class ReportGenerator
{
public function __construct(
private readonly FormatterInterface $formatter,
) {
}
public function generate(array $rows): string
{
$output = '';
foreach ($rows as $row) {
$output .= $this->formatter->format($row);
}
return $output;
}
}
// Swapping behavior at runtime, no new subclass, no abstract base class needed
$generator = new ReportGenerator(new JsonFormatter());
9. Abstrakte Klasse vs. Interface im direkten Vergleich
Die folgende Tabelle fasst die entscheidenden Unterschiede zwischen einer abstrakten Klasse und einem Interface entlang der Kriterien zusammen, die in der Praxis am häufigsten über die richtige Wahl entscheiden.
| Kriterium | Abstrakte Klasse | Interface |
|---|---|---|
| Enthält Implementierung | Ja, konkrete Methoden möglich | Nein, nur Signaturen |
| Konstruktor / Zustand | Ja, inklusive Properties | Nein, keine Properties |
| Mehrfachbeziehung | Nur eine Basisklasse (extends) | Beliebig viele (implements) |
| Typischer Einsatzzweck | Template Method, geteilte Basislogik | Lose Kopplung, Testbarkeit, Mehrfachverträge |
| Kopplung an Vorfahren | Enger, gemeinsame Vererbungslinie nötig | Lose, kein gemeinsamer Vorfahre nötig |
Die Tabelle zeigt, dass die Entscheidung selten eine reine Geschmacksfrage ist. Sobald Zustand oder konkreter Code geteilt werden muss, ist eine abstrakte Klasse strukturell überlegen. Sobald mehrere unabhängige Typen denselben Vertrag erfüllen sollen, ohne einen gemeinsamen Vorfahren zu teilen, ist ein Interface die einzig saubere Lösung, weil die Single-Inheritance-Beschränkung von PHP eine zweite abstrakte Basisklasse ohnehin ausschließt.
10. Zusammenfassung
Der Unterschied zwischen einer abstrakten Klasse und einem Interface ist kein Namensdetail, sondern eine strukturelle Entscheidung mit direkten Konsequenzen für Wartbarkeit und Kopplung. Eine abstrakte Klasse kann Konstruktoren, geteilten Zustand und konkrete Methoden bereitstellen und eignet sich damit hervorragend für das Template-Method-Pattern, in dem eine feste Ablaufstruktur garantiert werden soll. Ein Interface bleibt bewusst auf reine Vertragsdefinition beschränkt und ermöglicht dadurch lose Kopplung, Testbarkeit und die gleichzeitige Erfüllung mehrerer unabhängiger Verträge durch dieselbe Klasse.
Die Single-Inheritance-Beschränkung von PHP macht diese Entscheidung praktisch relevant: Wo mehr als eine Verhaltensdimension gebraucht wird, führt der Weg über Interfaces und Komposition, nicht über eine zweite abstrakte Basisklasse. Wer beide Konzepte bewusst kombiniert, eine abstrakte Klasse für geteilte Basislogik, ein Interface für den nach außen sichtbaren Vertrag, erhält Code, der sowohl Duplikation vermeidet als auch lose gekoppelt bleibt.
Abstrakte Klassen vs. Interfaces, Das Wichtigste auf einen Blick
Abstrakte Klasse
Konstruktoren, geteilter Zustand und konkrete Methoden möglich. Nur eine Basisklasse pro Klasse erlaubt.
Interface
Reiner Vertrag ohne Implementierung oder Zustand. Beliebig viele gleichzeitig implementierbar.
Template Method
final-Methode legt den Ablauf fest, abstract-Methoden liefern die fachliche Variation.
Entscheidungsregel
Geteilter Code: abstrakte Klasse. Unabhängige Typen mit gleichem Vertrag: Interface. Austauschbares Verhalten: Komposition.
11. FAQ: Abstrakte Klassen vs. Interfaces
1Hauptunterschied abstrakte Klasse vs. Interface?
2Kann eine abstrakte Klasse einen Konstruktor haben?
3Warum nur eine abstrakte Klasse, aber mehrere Interfaces?
4Was ist das Template-Method-Pattern?
5Beides gleichzeitig kombinierbar?
6Wann ist eine abstrakte Klasse falsch?
7Wann ist ein Interface falsch?
8Was ist das Fragile-Base-Class-Problem?
9Wann Komposition statt abstrakter Klasse?
10Kann ein Interface Konstanten enthalten?
Mironsoft
PHP-Entwicklung, Architektur-Review und OOP-Design
Klassenhierarchie in eurem PHP-Projekt sauber aufsetzen?
Wir helfen Teams, abstrakte Klassen und Interfaces gezielt einzusetzen, tiefe Vererbungsketten zu vermeiden und Verträge sauber von geteilter Implementierung zu trennen.
Architektur-Review
Bestehende Klassenhierarchien auf Fragile-Base-Class-Risiken prüfen
Refactoring
Tiefe Vererbungsketten zugunsten von Interfaces und Komposition auflösen
Entwurfs-Workshop
Template Method, Verträge und Komposition direkt am eigenen Domänenmodell