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.
Inhaltsverzeichnis
- 1. Welches Problem das Decorator Pattern wirklich löst
- 2. Grundstruktur: Component-Interface, ConcreteComponent, Decorator
- 3. Praxisbeispiel: Logging- und Caching-Decorator für einen Service
- 4. Decorators stapeln: Reihenfolge und ihre Konsequenzen
- 5. Abgrenzung zur Vererbung: warum Composition gewinnt
- 6. Der abstrakte Decorator als Basisklasse
- 7. Decorator Pattern in der Praxis: PSR-Middleware und HTTP-Clients
- 8. Häufige Fehler beim Decorator Pattern
- 9. Decorator im Vergleich zu verwandten Mustern
- 10. Zusammenfassung
- 11. FAQ
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.