Das Decorator Pattern in PHP: Verhalten zur Laufzeit erweitern statt vererben
AI generated
<?php
8.4
PHP · OOP-Patterns · Decorator Pattern · Composition
Das Decorator Pattern in PHP
Verhalten zur Laufzeit erweitern statt vererben

Das Decorator Pattern löst ein Problem, das reine Vererbung nicht sauber lösen kann: mehrere unabhängige Zusatzverhalten, etwa Logging, Caching und Validierung, flexibel und in beliebiger Reihenfolge kombinieren, ohne für jede Kombination eine eigene Unterklasse zu bauen. Dieser Artikel zeigt, wie das Decorator Pattern in PHP Interface-Konformität wahrt und wann es dem Aufblähen von Vererbungshierarchien vorzuziehen ist.

18 Min. Lesezeit Decorator · Composition over Inheritance · Wrapper PHP 8.2 · 8.3 · 8.4

1. Welches Problem das Decorator Pattern wirklich löst

Das Decorator Pattern löst ein sehr konkretes Problem: Wie fügt man einem Objekt zusätzliches Verhalten hinzu, ohne die Klasse selbst zu ändern und ohne für jede mögliche Kombination von Zusatzverhalten eine eigene Unterklasse zu erstellen? Ein klassisches Beispiel ist ein Bezahldienst, der optional geloggt, gecacht und validiert werden soll. Mit reiner Vererbung bräuchte man Klassen wie LoggingCachingPaymentService, CachingPaymentService, LoggingPaymentService und so weiter, eine kombinatorische Explosion, die mit jedem neuen Zusatzverhalten schlimmer wird.

Das Decorator Pattern umgeht dieses Problem, indem es Zusatzverhalten als eigenständige, austauschbare Wrapper-Objekte modelliert, die alle dasselbe Interface implementieren wie das Objekt, das sie umschließen. Jeder Decorator hält eine Referenz auf ein Objekt desselben Interface-Typs, ruft dessen Methode auf und ergänzt eigenes Verhalten davor, danach oder um den Aufruf herum. Aus Sicht des Aufrufers ist ein dekoriertes Objekt nicht von einem undekorierten zu unterscheiden, beide implementieren dasselbe Interface.

Namensgebend ist die Analogie zum Dekorieren: Ein Weihnachtsbaum bleibt ein Baum, egal wie viele Lichterketten und Kugeln man hinzufügt, jede Dekoration lässt sich unabhängig entfernen, ohne den Baum selbst zu verändern. Das Decorator Pattern überträgt dieses Prinzip auf Objekte und macht Composition over Inheritance dabei konkret erfahrbar, statt es nur als abstraktes Prinzip zu zitieren.

2. Grundstruktur: Component-Interface, ConcreteComponent, Decorator

Die Struktur des Decorator Pattern besteht aus drei Teilen. Erstens ein Component-Interface, das die Methode definiert, die dekoriert werden soll. Zweitens eine ConcreteComponent-Klasse, die dieses Interface mit der eigentlichen Basisfunktionalität implementiert. Drittens eine oder mehrere Decorator-Klassen, die ebenfalls das Component-Interface implementieren, zusätzlich aber eine Instanz desselben Interface-Typs im Konstruktor entgegennehmen und intern speichern.

Der entscheidende Kniff ist, dass ein Decorator selbst wieder als Component-Interface gilt. Dadurch lassen sich Decorators beliebig ineinander verschachteln, ein Decorator kann ein anderes Decorator-Objekt umschließen, das wiederum die ConcreteComponent umschließt. Diese Verschachtelungsfähigkeit ist der Kern des Decorator Pattern und unterscheidet es von einer bloßen Wrapper-Klasse, die nur eine feste, nicht kombinierbare Zusatzfunktion bereitstellt.


<?php

declare(strict_types=1);

/**
 * Component interface shared by the concrete service and every decorator.
 */
interface PaymentServiceInterface
{
    public function charge(int $amountCents, string $currency): bool;
}

/**
 * ConcreteComponent: the actual base implementation.
 */
final class StripePaymentService implements PaymentServiceInterface
{
    public function charge(int $amountCents, string $currency): bool
    {
        // Simplified: real implementation talks to the Stripe API
        return $amountCents > 0;
    }
}

Diese beiden Klassen bilden das Fundament. Ohne jeden Decorator funktioniert StripePaymentService bereits vollständig eigenständig. Erst im nächsten Abschnitt kommen die eigentlichen Decorators hinzu, die Zusatzverhalten um diese Basisimplementierung herum ergänzen, ohne sie selbst anzufassen.

3. Praxisbeispiel: Logging- und Caching-Decorator für einen Service

Der praktische Nutzen des Decorator Pattern zeigt sich am deutlichsten an einem konkreten Beispiel mit zwei unabhängigen Zusatzverhalten. Ein Logging-Decorator protokolliert jeden Aufruf mit Parametern und Ergebnis, unabhängig davon, welcher konkrete Service dahinterliegt. Ein Caching-Decorator vermeidet wiederholte teure Aufrufe, indem er Ergebnisse für identische Parameter zwischenspeichert. Beide Decorators implementieren dasselbe PaymentServiceInterface wie der Service selbst und können unabhängig voneinander auf jeden beliebigen konkreten Service angewendet werden.

Wichtig beim Decorator Pattern ist, dass jeder Decorator ausschließlich über das Interface mit dem umschlossenen Objekt kommuniziert, niemals über konkrete Klassen. Dadurch bleibt ein Logging-Decorator vollständig unabhängig davon, ob er einen echten StripePaymentService oder einen anderen Decorator umschließt. Diese Interface-Konformität ist die Grundvoraussetzung dafür, dass sich Decorators beliebig kombinieren lassen.


<?php

declare(strict_types=1);

/**
 * Decorator: adds logging around any PaymentServiceInterface implementation.
 */
final class LoggingPaymentDecorator implements PaymentServiceInterface
{
    public function __construct(
        private readonly PaymentServiceInterface $inner,
    ) {
    }

    public function charge(int $amountCents, string $currency): bool
    {
        error_log(sprintf('Charging %d %s', $amountCents, $currency));

        $result = $this->inner->charge($amountCents, $currency);

        error_log(sprintf('Charge result: %s', $result ? 'success' : 'failure'));

        return $result;
    }
}

/**
 * Decorator: caches successful charges for identical parameters.
 */
final class CachingPaymentDecorator implements PaymentServiceInterface
{
    /** @var array<string, bool> */
    private array $cache = [];

    public function __construct(
        private readonly PaymentServiceInterface $inner,
    ) {
    }

    public function charge(int $amountCents, string $currency): bool
    {
        $key = $amountCents . '|' . $currency;

        if (array_key_exists($key, $this->cache)) {
            return $this->cache[$key];
        }

        return $this->cache[$key] = $this->inner->charge($amountCents, $currency);
    }
}

// Decorators are stacked around the concrete service
$service = new LoggingPaymentDecorator(
    new CachingPaymentDecorator(
        new StripePaymentService(),
    ),
);

$service->charge(2500, 'EUR');

Der Aufrufer instanziiert am Ende nur eine Kette aus Konstruktoraufrufen, sieht aber ausschließlich das gemeinsame PaymentServiceInterface. Ob ein, zwei oder fünf Decorators involviert sind, spielt für den Code, der $service->charge(...) aufruft, keine Rolle. Genau diese Transparenz ist der zentrale Vorteil des Decorator Pattern gegenüber jeder Vererbungslösung.

4. Decorators stapeln: Reihenfolge und ihre Konsequenzen

Die Reihenfolge, in der Decorators gestapelt werden, ist keine Formalie, sondern verändert das tatsächliche Verhalten. Im obigen Beispiel liegt Caching innen und Logging außen, das bedeutet, jeder Aufruf wird geloggt, auch wenn das Ergebnis aus dem Cache kommt, aber der eigentliche StripePaymentService-Aufruf wird bei einem Cache-Treffer übersprungen. Würde man die Reihenfolge umdrehen, also Logging innen und Caching außen, würde ein gecachtes Ergebnis den Logging-Decorator gar nicht mehr erreichen, weil der Caching-Decorator den inneren Aufruf komplett überspringt.

Diese Reihenfolgeabhängigkeit ist bei jedem Einsatz des Decorator Pattern bewusst zu planen. Als Faustregel gilt: Decorators, die den eigentlichen Aufruf möglicherweise komplett verhindern, etwa Caching oder Rate Limiting, sollten weiter innen liegen als Decorators, die Beobachtungsverhalten wie Logging oder Metriken bereitstellen, es sei denn, das gewünschte Verhalten ist explizit, dass übersprungene Aufrufe auch nicht beobachtet werden sollen.

Ein zweites Detail betrifft Exceptions. Wirft der innerste Service eine Exception, propagiert sie durch alle umschließenden Decorators nach außen, es sei denn, ein Decorator fängt sie explizit ab. Ein Retry-Decorator etwa würde genau das tun: die Exception des inneren Aufrufs abfangen, den Aufruf mehrfach wiederholen und erst nach Erschöpfen der Versuche die Exception weiterreichen. Auch hier bestimmt die Position im Stapel, welche anderen Decorators von diesem Retry-Verhalten betroffen sind.

5. Abgrenzung zur Vererbung: warum Composition gewinnt

Der direkte Vergleich zur Vererbung macht deutlich, warum das Decorator Pattern in den meisten Fällen die bessere Wahl ist. Mit Vererbung ist die Menge der möglichen Kombinationen zur Kompilierzeit fest verdrahtet, jede neue Kombination erfordert eine neue Klasse. Mit dem Decorator Pattern entsteht die Kombination zur Laufzeit durch die Reihenfolge der Konstruktoraufrufe, ganz ohne neue Klassen. Eine Konfigurationsdatei oder ein DI-Container kann sogar entscheiden, welche Decorators in welcher Reihenfolge angewendet werden, ohne dass der PHP-Code selbst angepasst werden muss.

Vererbung eignet sich weiterhin gut für eine Ist-eine-Beziehung, bei der eine Unterklasse tatsächlich eine speziellere Variante der Elternklasse ist. Das Decorator Pattern eignet sich für eine Hat-eine-Beziehung, bei der Zusatzverhalten unabhängig vom Kern der Funktionalität existiert und potenziell auf beliebige Implementierungen desselben Interface angewendet werden soll. Wer beide Situationen verwechselt und Zusatzverhalten über Vererbung statt über das Decorator Pattern modelliert, erzeugt starre Klassenhierarchien, die mit jeder neuen Anforderung schwerer zu erweitern werden.

6. Der abstrakte Decorator als Basisklasse

Sobald ein Projekt mehrere Decorators für dasselbe Interface benötigt, lohnt sich eine abstrakte Basisklasse, die das Delegieren an das umschlossene Objekt für alle nicht überschriebenen Methoden übernimmt. Das ist besonders relevant bei Interfaces mit mehreren Methoden, weil ohne Basisklasse jeder Decorator alle Interface-Methoden explizit weiterleiten müsste, selbst wenn er nur eine einzige Methode tatsächlich verändern will.


<?php

declare(strict_types=1);

interface NotifierInterface
{
    public function send(string $recipient, string $message): bool;
    public function supports(string $channel): bool;
}

/**
 * Abstract decorator: forwards every call by default, subclasses override selectively.
 */
abstract class NotifierDecorator implements NotifierInterface
{
    public function __construct(
        protected readonly NotifierInterface $inner,
    ) {
    }

    public function send(string $recipient, string $message): bool
    {
        return $this->inner->send($recipient, $message);
    }

    public function supports(string $channel): bool
    {
        return $this->inner->supports($channel);
    }
}

/**
 * Concrete decorator: only overrides send(), supports() is inherited unchanged.
 */
final class RateLimitedNotifierDecorator extends NotifierDecorator
{
    private int $sentInLastMinute = 0;

    public function send(string $recipient, string $message): bool
    {
        if ($this->sentInLastMinute >= 10) {
            return false;
        }

        $this->sentInLastMinute++;
        return parent::send($recipient, $message);
    }
}

Diese abstrakte Basisklasse spart in größeren Systemen mit vielen Decorators erheblich Code, weil jeder konkrete Decorator nur die Methoden überschreiben muss, die er tatsächlich verändern will. Das Decorator Pattern bleibt dabei unverändert im Prinzip, die Basisklasse ist reine Implementierungshilfe.

7. Decorator Pattern in der Praxis: PSR-Middleware und HTTP-Clients

Das Decorator Pattern ist in modernen PHP-Ökosystemen allgegenwärtig, auch wenn es nicht immer explizit so benannt wird. PSR-15-Middleware in Frameworks wie Slim oder Mezzio folgt exakt diesem Muster: Jede Middleware implementiert dasselbe Interface, umschließt die nächste Middleware in der Kette und kann Verhalten vor oder nach dem eigentlichen Request-Handling ergänzen, etwa Authentifizierung, CORS-Header oder Request-Logging. Auch Guzzle-Handler-Stacks nutzen das Decorator Pattern, um Middleware wie Retry-Logik oder Request-Signierung um den eigentlichen HTTP-Client zu legen.

Diese verbreitete Nutzung zeigt, dass das Decorator Pattern kein akademisches Konstrukt ist, sondern die Grundlage vieler produktiver PHP-Bibliotheken bildet. Wer versteht, wie ein einfacher Logging-Decorator funktioniert, versteht damit auch die Grundstruktur von PSR-15-Middleware-Pipelines, nur mit anderen Methodennamen und einer ServerRequestInterface statt eines einfachen charge()-Aufrufs.

8. Häufige Fehler beim Decorator Pattern

Der häufigste Fehler ist, dass ein Decorator zusätzliche, interface-fremde öffentliche Methoden anbietet, auf die der Aufrufer angewiesen ist. Sobald Code explizit auf die konkrete Decorator-Klasse statt auf das Interface zugreift, geht die Austauschbarkeit verloren, die das gesamte Muster erst rechtfertigt. Ein zweiter Fehler ist, Zustand im Decorator zu halten, der eigentlich zur ConcreteComponent gehört, etwa Konfigurationswerte, die unabhängig davon gültig sein sollten, wie viele Decorators aktuell aktiv sind.

Ein dritter, subtilerer Fehler betrifft Typprüfungen mit instanceof gegen eine konkrete Decorator-Klasse innerhalb von Aufrufercode. Das bricht die Abstraktion, weil der Aufrufer dann wissen muss, welche konkreten Decorators aktuell im Stapel liegen, statt sich ausschließlich auf das Interface zu verlassen. Wenn ein Aufrufer wirklich wissen muss, ob ein bestimmter Decorator aktiv ist, ist das oft ein Zeichen dafür, dass diese Information stattdessen über das Interface selbst, etwa eine zusätzliche Methode, transportiert werden sollte.

9. Decorator im Vergleich zu verwandten Mustern

Das Decorator Pattern wird häufig mit strukturell ähnlichen Mustern verwechselt, die aber unterschiedliche Ziele verfolgen. Die folgende Tabelle stellt die wichtigsten Unterschiede gegenüber.

Muster Ziel Interface identisch? Kombinierbar
Decorator Verhalten zur Laufzeit ergänzen Ja Beliebig stapelbar
Adapter Inkompatible Interfaces verbinden Nein Meist einmalig
Proxy Zugriffskontrolle, Lazy Loading Ja Meist einmalig
Strategy Algorithmus austauschbar machen Eigenes Strategy-Interface Nicht stapelbar
Middleware-Pipeline Request-Verarbeitung erweitern Ja Beliebig stapelbar

Der entscheidende Unterschied zwischen Decorator und Proxy ist die Absicht: Ein Proxy kontrolliert typischerweise den Zugriff auf ein Objekt, etwa durch Lazy Loading oder Zugriffsrechte, während ein Decorator gezielt zusätzliches Verhalten hinzufügt. Technisch sind beide Muster fast identisch aufgebaut, der Unterschied liegt im Zweck, nicht in der Struktur.

Mironsoft

PHP-Architektur, Objektdesign und wartbare Backend-Systeme

Aufgeblähte Vererbungshierarchien statt flexibler Composition?

Wir analysieren bestehende PHP-Klassenhierarchien und bauen Decorator-basierte Lösungen für Logging, Caching, Retry-Logik und Validierung, die sich flexibel kombinieren lassen.

Architektur-Review

Vererbungshierarchien auf Decorator-Potenzial prüfen

Decorator-Implementierung

Logging, Caching und Retry-Decorators sauber aufbauen

DI-Container-Konfiguration

Decorator-Stapel zentral und konfigurierbar verdrahten

10. Zusammenfassung

Das Decorator Pattern löst die kombinatorische Explosion, die reine Vererbung bei mehreren unabhängigen Zusatzverhalten erzeugt. Statt für jede Kombination eine eigene Unterklasse zu bauen, implementieren alle Decorators dasselbe Interface wie die ConcreteComponent und lassen sich in beliebiger Reihenfolge um sie herum stapeln. Die Reihenfolge im Stapel ist keine Formalie, sie bestimmt, welcher Decorator welchen anderen tatsächlich beeinflusst, insbesondere bei Caching, Rate Limiting und Retry-Logik.

Eine abstrakte Decorator-Basisklasse spart Code bei Interfaces mit vielen Methoden, indem sie das Weiterleiten an das umschlossene Objekt übernimmt. In modernen PHP-Anwendungen begegnet das Decorator Pattern praktisch überall dort, wo PSR-15-Middleware oder Guzzle-Handler-Stacks verwendet werden. Wer das Decorator Pattern verstanden hat, versteht damit auch die Grundstruktur dieser weit verbreiteten Werkzeuge.

Das Decorator Pattern in PHP — Das Wichtigste auf einen Blick

Grundstruktur

Component-Interface, ConcreteComponent und ein oder mehrere Decorators, die alle dasselbe Interface implementieren.

Composition over Inheritance

Zusatzverhalten wird zur Laufzeit kombiniert, statt eine Unterklasse pro Kombination zu erzeugen.

Reihenfolge zählt

Die Stapelreihenfolge bestimmt, welcher Decorator welchen anderen beeinflusst, besonders bei Caching und Retry.

Praxisbezug

PSR-15-Middleware und Guzzle-Handler-Stacks basieren auf demselben Prinzip.

11. FAQ: Decorator Pattern in PHP

1Decorator vs. Vererbung?
Vererbung legt Kombinationen zur Kompilierzeit fest. Decorator kombiniert Zusatzverhalten zur Laufzeit ohne neue Klassen.
2Gleiches Interface Pflicht?
Ja, Grundvoraussetzung. Nur so lassen sich Decorators beliebig verschachteln und austauschen.
3Reihenfolge wichtig?
Ja, erheblich. Caching innen und Logging außen protokolliert auch Cache-Treffer, umgekehrt nicht.
4Decorator vs. Proxy?
Strukturell fast identisch, der Unterschied liegt in der Absicht: Zugriffskontrolle beim Proxy, Verhaltenserweiterung beim Decorator.
5Abstrakte Basisklasse wofür?
Übernimmt Weiterleitung aller Methoden, konkrete Decorators überschreiben nur, was sie wirklich ändern wollen.
6PSR-15-Middleware ein Beispiel?
Ja, jede Middleware umschließt die nächste und implementiert dasselbe Interface, genau das Decorator-Prinzip.
7Exceptions im Stapel?
Propagieren nach außen, es sei denn ein Decorator fängt sie ab, etwa ein Retry-Decorator.
8Wann Vererbung vorziehen?
Bei echter Ist-eine-Beziehung. Für unabhängiges Zusatzverhalten ist Decorator die bessere Wahl.
9Häufigster Fehler?
instanceof-Prüfungen gegen konkrete Decorator-Klassen brechen die Austauschbarkeit des Interface.
10Performance-Auswirkung?
Jeder Decorator ein zusätzlicher Methodenaufruf, in der Praxis meist vernachlässigbar.