Abstrakte Klassen vs. Interfaces: die richtige Wahl im Entwurf
AI generated
<?php
8.4
PHP 8.4 · Abstrakte Klassen · Interfaces · Entwurf
Abstrakte Klassen vs. Interfaces
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.

13 Min. Lesezeit Template Method · abstract · implements PHP 8.4 · OOP-Grundlagen

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?
Interface: reiner Vertrag ohne Implementierung. Abstrakte Klasse: konkrete Methoden, Konstruktor und geteilter Zustand möglich, nur einzelne Methoden bleiben abstract.
2Kann eine abstrakte Klasse einen Konstruktor haben?
Ja. Unterklassen erben ihn und müssen parent::__construct() aufrufen. Ein Interface kann keine Konstruktorlogik vorschreiben.
3Warum nur eine abstrakte Klasse, aber mehrere Interfaces?
Vermeidet das Diamond-Problem der Mehrfachvererbung. Interfaces bringen keine Implementierung mit, daher kein Konflikt bei mehreren gleichzeitig.
4Was ist das Template-Method-Pattern?
final-Methode legt den festen Ablauf fest, ruft dabei abstract Hook-Methoden auf, die jede Unterklasse individuell implementiert.
5Beides gleichzeitig kombinierbar?
Ja, ein häufiges Muster. Die abstrakte Klasse implementiert einen Teil der Interface-Methoden konkret, der Rest bleibt abstract.
6Wann ist eine abstrakte Klasse falsch?
Wenn sie nur Konstanten bündelt ohne echte geteilte Implementierung. Dann sind Interface mit Konstanten oder enum passender.
7Wann ist ein Interface falsch?
Wenn geteilte Implementierung nötig wäre. Jede Klasse müsste dieselbe Logik separat schreiben, das führt zu Duplikation.
8Was ist das Fragile-Base-Class-Problem?
Tiefe Vererbungsketten riskieren, dass eine Änderung an gemeinsamer Logik alle Unterklassen unbeabsichtigt beeinflusst.
9Wann Komposition statt abstrakter Klasse?
Wenn Verhalten zur Laufzeit austauschbar sein soll. Die Klasse hält dann eine Referenz auf ein Interface statt fest zu erben.
10Kann ein Interface Konstanten enthalten?
Ja, aber keine Properties und keine Methodenimplementierung. Für reine Konstantenbehälter oft passender als eine abstrakte Klasse.

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