Familien verwandter Objekte sauber erzeugen
Sobald ein System mehrere zueinander passende Objekte austauschbar erzeugen muss, etwa Zahlungsanbieter mit passendem Client, Validator und Logger, reicht eine einfache Factory-Methode nicht mehr aus. Das Abstract Factory Pattern kapselt genau diese Erzeugung ganzer Produktfamilien hinter einer gemeinsamen, typsicheren Schnittstelle.
Inhaltsverzeichnis
- 1. Was das Abstract Factory Pattern wirklich löst
- 2. Grundstruktur: Produkte, Fabriken und Interfaces
- 3. Praxisbeispiel: austauschbare Zahlungsanbieter
- 4. Abstract Factory für Multi-Mandanten-Systeme
- 5. Integration mit Dependency-Injection-Containern
- 6. Neue Produktfamilien ergänzen ohne Bestandscode zu ändern
- 7. Testbarkeit: Fake-Fabriken für Unit-Tests
- 8. Typische Fehler und Überengineering vermeiden
- 9. Abstract Factory im Vergleich zu Factory Method und Builder
- 10. Zusammenfassung
- 11. FAQ
1. Was das Abstract Factory Pattern wirklich löst
Das Abstract Factory Pattern gehört zur Familie der Kreationsmuster und löst ein spezifisches Problem: Ein Client-Code soll ganze Familien miteinander kompatibler Objekte erzeugen, ohne die konkreten Klassen dieser Objekte zu kennen. Der entscheidende Unterschied zu einer einfachen Factory-Methode ist die Betonung auf "Familien": Nicht ein einzelnes Objekt wird erzeugt, sondern ein zusammenhängendes Set mehrerer Objekte, die zueinander passen müssen und niemals gemischt werden dürfen.
Ein klassisches Beispiel verdeutlicht das Problem: Ein E-Commerce-System unterstützt mehrere Zahlungsanbieter wie Stripe und PayPal. Jeder Anbieter benötigt einen eigenen API-Client, einen eigenen Request-Validator und einen eigenen Response-Parser. Diese drei Objekte müssen zusammenpassen, ein Stripe-Client mit einem PayPal-Validator zu kombinieren ergibt keinen funktionierenden Code. Das Abstract Factory Pattern stellt sicher, dass genau diese Konsistenz garantiert bleibt, indem eine einzige Fabrik-Instanz für die gesamte Familie zuständig ist.
In diesem Artikel bauen wir das Abstract Factory Pattern Schritt für Schritt in PHP auf, von der grundlegenden Struktur über ein vollständiges Zahlungsanbieter-Beispiel bis zur Integration in Dependency-Injection-Container. Am Ende weißt du genau, wann sich der zusätzliche Strukturierungsaufwand lohnt und wann eine einfachere Lösung ausreicht.
2. Grundstruktur: Produkte, Fabriken und Interfaces
Die Struktur des Abstract Factory Pattern besteht aus drei Ebenen. Erstens: Abstrakte Produkt-Interfaces, die definieren, welche Operationen jedes Produkt in der Familie unterstützen muss, unabhängig von der konkreten Implementierung. Zweitens: Konkrete Produktklassen, die diese Interfaces für eine bestimmte Familie implementieren, etwa alle Stripe-bezogenen Klassen. Drittens: Ein abstraktes Fabrik-Interface mit einer Methode pro Produkttyp, sowie konkrete Fabrikklassen, die für jede Familie die passenden konkreten Produkte instanziieren.
Der Client-Code arbeitet ausschließlich mit dem abstrakten Fabrik-Interface und den abstrakten Produkt-Interfaces, niemals mit konkreten Klassen. Diese strikte Trennung ist der Kern des Abstract Factory Pattern: Der Client weiß nur, dass er eine PaymentProviderFactory hat, die einen PaymentClient, einen PaymentValidator und einen PaymentResponseParser liefert, aber nicht, ob dahinter Stripe, PayPal oder ein Test-Double steckt. Diese Entkopplung ist es, die das Austauschen ganzer Produktfamilien zur Laufzeit oder Konfigurationszeit ermöglicht, ohne dass der Client-Code angepasst werden muss.
<?php
declare(strict_types=1);
// Abstract product interfaces — the "family" contract
interface PaymentClient
{
public function charge(int $amountInCents, string $currency): PaymentResult;
}
interface PaymentValidator
{
public function validate(array $requestPayload): bool;
}
interface PaymentResponseParser
{
public function parse(string $rawResponse): PaymentResult;
}
// Abstract factory interface — one creation method per product type
interface PaymentProviderFactory
{
public function createClient(): PaymentClient;
public function createValidator(): PaymentValidator;
public function createResponseParser(): PaymentResponseParser;
}
3. Praxisbeispiel: austauschbare Zahlungsanbieter
Mit den Interfaces aus dem vorherigen Abschnitt lässt sich das Abstract Factory Pattern nun mit konkreten Implementierungen füllen. Jede konkrete Fabrik, etwa StripeProviderFactory und PayPalProviderFactory, kapselt genau die Produkte, die zueinander passen. Der entscheidende Vorteil zeigt sich beim Aufrufercode: Ein Order-Service, der mit dem abstrakten PaymentProviderFactory-Interface arbeitet, muss keine Zeile Code ändern, wenn ein neuer Zahlungsanbieter hinzukommt oder ein bestehender ausgetauscht wird.
Diese Austauschbarkeit ist der praktische Kern des Abstract Factory Pattern: Die Entscheidung, welche konkrete Fabrik verwendet wird, fällt an genau einer zentralen Stelle, etwa in der Bootstrapping-Phase der Anwendung basierend auf einer Konfigurationseinstellung. Der restliche Anwendungscode bleibt vollständig unabhängig von dieser Entscheidung und arbeitet ausschließlich mit den abstrakten Interfaces.
<?php
declare(strict_types=1);
final class StripeProviderFactory implements PaymentProviderFactory
{
public function __construct(private readonly string $apiKey)
{
}
public function createClient(): PaymentClient
{
return new StripeClient($this->apiKey);
}
public function createValidator(): PaymentValidator
{
return new StripeSignatureValidator($this->apiKey);
}
public function createResponseParser(): PaymentResponseParser
{
return new StripeResponseParser();
}
}
final class PayPalProviderFactory implements PaymentProviderFactory
{
public function __construct(
private readonly string $clientId,
private readonly string $clientSecret,
) {
}
public function createClient(): PaymentClient
{
return new PayPalClient($this->clientId, $this->clientSecret);
}
public function createValidator(): PaymentValidator
{
return new PayPalWebhookValidator($this->clientSecret);
}
public function createResponseParser(): PaymentResponseParser
{
return new PayPalResponseParser();
}
}
// Client code depends only on the abstract factory interface
final class CheckoutService
{
private PaymentClient $client;
private PaymentValidator $validator;
private PaymentResponseParser $parser;
public function __construct(PaymentProviderFactory $factory)
{
$this->client = $factory->createClient();
$this->validator = $factory->createValidator();
$this->parser = $factory->createResponseParser();
}
public function processPayment(int $amountInCents, string $currency): PaymentResult
{
return $this->client->charge($amountInCents, $currency);
}
}
4. Abstract Factory für Multi-Mandanten-Systeme
Ein weiteres wichtiges Anwendungsfeld für das Abstract Factory Pattern sind Multi-Mandanten-Systeme, bei denen jeder Mandant eine eigene Konfiguration von zueinander passenden Services benötigt. Ein Mandant könnte etwa einen bestimmten E-Mail-Provider, einen bestimmten SMS-Gateway und ein bestimmtes Rechnungsformat verwenden, während ein anderer Mandant eine komplett andere Kombination benötigt. Das Abstract Factory Pattern bildet diese Mandanten-spezifischen Kombinationen exakt ab, indem pro Mandant eine eigene konkrete Fabrik existiert oder zur Laufzeit konfiguriert wird.
In der Praxis kombiniert man das Abstract Factory Pattern in solchen Szenarien häufig mit einer Registry oder einem einfachen Factory-of-Factories-Mechanismus, der anhand einer Mandanten-ID die passende konkrete Fabrik zurückgibt. Diese Kombination hält die Mandanten-spezifische Konfigurationslogik an einer einzigen, gut testbaren Stelle, statt sie über den gesamten Anwendungscode zu verteilen, wo an jeder Stelle erneut geprüft werden müsste, welcher Mandant gerade aktiv ist.
<?php
declare(strict_types=1);
interface TenantServiceFactory
{
public function createMailer(): MailerInterface;
public function createInvoiceFormatter(): InvoiceFormatterInterface;
}
final class TenantServiceFactoryRegistry
{
/** @var array<string, TenantServiceFactory> */
private array $factories = [];
public function register(string $tenantId, TenantServiceFactory $factory): void
{
$this->factories[$tenantId] = $factory;
}
public function resolve(string $tenantId): TenantServiceFactory
{
return $this->factories[$tenantId]
?? throw new RuntimeException("No factory registered for tenant: {$tenantId}");
}
}
// Bootstrapping: register concrete factories once at startup
$registry = new TenantServiceFactoryRegistry();
$registry->register('acme-corp', new AcmeTenantServiceFactory());
$registry->register('globex-inc', new GlobexTenantServiceFactory());
// Runtime resolution: application code stays tenant-agnostic
$factory = $registry->resolve($currentTenantId);
$mailer = $factory->createMailer();
5. Integration mit Dependency-Injection-Containern
In modernen PHP-Anwendungen wird das Abstract Factory Pattern selten manuell verdrahtet, sondern über einen Dependency-Injection-Container konfiguriert. Statt den Client-Code direkt eine konkrete Fabrik instanziieren zu lassen, registriert man im Container eine Bindung vom abstrakten PaymentProviderFactory-Interface auf die konkrete Implementierung, die aktuell aktiv sein soll. Der Container übernimmt dann die Auflösung, und der Client-Code fordert einfach eine Instanz des Interfaces an.
Dieser Ansatz kombiniert die Stärken des Abstract Factory Pattern mit denen der Dependency Injection: Die Produktfamilie bleibt konsistent und typsicher, während die Entscheidung, welche konkrete Familie verwendet wird, vollständig in die Konfiguration des Containers ausgelagert ist. Ein Wechsel des Zahlungsanbieters für die gesamte Anwendung reduziert sich dann auf eine einzige Zeile in der Container-Konfiguration, ohne dass irgendein anderer Code angefasst werden muss. Genau diese Eigenschaft macht das Abstract Factory Pattern zu einem beliebten Baustein in Symfony- und PSR-11-kompatiblen Anwendungen.
6. Neue Produktfamilien ergänzen ohne Bestandscode zu ändern
Ein zentrales Versprechen des Abstract Factory Pattern ist die Einhaltung des Open-Closed-Prinzips: offen für Erweiterung, geschlossen für Änderung. Eine neue Produktfamilie hinzuzufügen, etwa die Unterstützung für einen dritten Zahlungsanbieter wie Klarna, erfordert lediglich das Implementieren der drei Produktinterfaces und der Fabrikklasse für diesen neuen Anbieter. Kein bestehender Code muss geändert werden, weder die CheckoutService-Klasse noch andere Fabriken sind betroffen.
Diese Eigenschaft unterscheidet das Abstract Factory Pattern deutlich von einer großen switch-Anweisung, die anhand eines Anbieter-Strings die passenden Objekte instanziiert. Bei einem solchen switch müsste jede neue Produktfamilie an genau dieser zentralen Stelle ergänzt werden, was mit wachsender Anzahl an Anbietern zu einer immer länger werdenden, unübersichtlichen Methode führt. Das Abstract Factory Pattern verteilt diese Verantwortung stattdessen auf separate, in sich geschlossene Klassen, von denen jede genau eine Produktfamilie kennt.
7. Testbarkeit: Fake-Fabriken für Unit-Tests
Ein oft unterschätzter Vorteil des Abstract Factory Pattern zeigt sich beim Schreiben von Unit-Tests. Da der Client-Code ausschließlich mit dem abstrakten Fabrik-Interface arbeitet, lässt sich für Tests eine FakePaymentProviderFactory implementieren, die Test-Doubles statt echter API-Clients zurückgibt. Der CheckoutService aus dem Praxisbeispiel benötigt für seine Tests keine Stripe- oder PayPal-Zugangsdaten, sondern erhält beim Testen einfach die Fake-Fabrik injiziert.
Diese Testbarkeit ist ein direktes Resultat der strikten Trennung zwischen abstrakten Interfaces und konkreten Implementierungen, die das Abstract Factory Pattern erzwingt. Ohne dieses Muster würde ein Test häufig echte Netzwerkaufrufe mocken müssen, verstreut über viele einzelne Methodenaufrufe. Mit dem Abstract Factory Pattern reicht ein einziger Austausch der Fabrik-Instanz, um die komplette Produktfamilie durch Test-Doubles zu ersetzen, was Tests erheblich schneller und stabiler macht.
8. Typische Fehler und Überengineering vermeiden
Der häufigste Fehler beim Einsatz des Abstract Factory Pattern ist der Einsatz für Situationen, in denen gar keine echte Produktfamilie existiert, sondern nur ein einzelnes Objekt erzeugt werden muss. Für ein einzelnes Produkt reicht eine einfache Factory-Methode oder sogar ein direkter Constructor-Aufruf völlig aus. Das Abstract Factory Pattern rechtfertigt seinen zusätzlichen strukturellen Aufwand nur dann, wenn tatsächlich mehrere zusammengehörige Objekte konsistent ausgetauscht werden müssen.
Ein zweiter Fehler ist eine zu granulare Aufteilung der Produktfamilie. Werden Produkte in die Fabrik aufgenommen, die in der Praxis nie unabhängig von anderen ausgetauscht werden, wächst das Interface unnötig und erschwert die Implementierung neuer Fabriken. Ein drittes Problem entsteht, wenn Fabriken selbst wieder Zustand halten, der zwischen mehreren Erzeugungsaufrufen geteilt werden soll, das Abstract Factory Pattern ist für zustandslose, wiederholt aufrufbare Erzeugung konzipiert, nicht für Singleton-artige Zustandsverwaltung, wofür andere Muster besser geeignet sind.
9. Abstract Factory im Vergleich zu Factory Method und Builder
Das Abstract Factory Pattern wird häufig mit verwandten Kreationsmustern verwechselt. Die folgende Tabelle stellt die wichtigsten Unterschiede zusammen und hilft bei der Wahl des richtigen Musters für den jeweiligen Anwendungsfall.
| Kriterium | Factory Method | Abstract Factory | Builder |
|---|---|---|---|
| Erzeugt | Ein einzelnes Objekt | Familie mehrerer verwandter Objekte | Ein komplexes Objekt schrittweise |
| Fokus | Welche Klasse instanziieren | Konsistenz zwischen Produkten sichern | Konstruktionsschritte und -reihenfolge |
| Typische Umsetzung | Eine Methode, oft mit Late Static Binding | Interface mit mehreren Erzeugungsmethoden | Fluent-Interface mit with-Methoden |
| Austauschbarkeit | Eine Implementierung pro Aufruf | Ganze Familie auf einmal austauschbar | Nicht der Fokus des Musters |
| Komplexität | Gering | Mittel bis hoch | Mittel |
Diese Gegenüberstellung zeigt, warum das Abstract Factory Pattern speziell für Situationen mit mehreren zusammengehörigen Produkten geeignet ist, während die einfachere Factory-Methode für ein einzelnes Objekt genügt und der Builder eher bei komplexer, schrittweiser Konstruktion eines einzelnen Objekts zum Einsatz kommt. Die Wahl zwischen diesen Mustern sollte immer anhand der tatsächlichen Anforderung getroffen werden, nicht anhand persönlicher Vorliebe für ein bestimmtes Muster.
Mironsoft
PHP-Architektur, Design Patterns und Multi-Mandanten-Systeme
Austauschbare Zahlungsanbieter oder Mandanten-Konfigurationen aufbauen?
Wir entwerfen Abstract-Factory-Strukturen für austauschbare Provider-Familien, integrieren sie sauber in eure Dependency-Injection-Konfiguration und sorgen für vollständig testbaren, erweiterbaren Code ohne Überengineering.
Architektur-Design
Produktfamilien identifizieren und passende Kreationsmuster auswählen
DI-Integration
Abstract-Factory-Bindungen sauber in PSR-11-Container konfigurieren
Testabdeckung
Fake-Fabriken und Test-Doubles für schnelle, stabile Unit-Tests
10. Zusammenfassung
Das Abstract Factory Pattern löst genau ein Problem: Ganze Familien zueinander passender Objekte konsistent und austauschbar zu erzeugen, ohne dass der Client-Code konkrete Klassen kennen muss. Die Struktur aus abstrakten Produkt-Interfaces, konkreten Produktimplementierungen und einer abstrakten Fabrik mit mehreren Erzeugungsmethoden erzwingt genau diese Konsistenz, wo eine einfache Factory-Methode versagen würde, weil sie nur ein einzelnes Objekt betrachtet.
Die größte Stärke des Abstract Factory Pattern liegt in der Kombination aus Erweiterbarkeit, Testbarkeit und sauberer Integration in Dependency-Injection-Container. Die größte Gefahr liegt im Überengineering: Wird das Muster für einzelne, unabhängige Objekte eingesetzt, entsteht unnötiger struktureller Aufwand ohne echten Mehrwert. Wer die Entscheidung anhand der tatsächlichen Produktfamilien-Struktur trifft, statt aus reiner Musterliebe, nutzt das Abstract Factory Pattern dort, wo es seine Stärken wirklich ausspielt.
Abstract Factory in PHP — Das Wichtigste auf einen Blick
Kernidee
Eine Fabrik erzeugt mehrere zueinander passende Produkte als konsistente, austauschbare Familie.
Einsatzgebiet
Austauschbare Zahlungsanbieter, Multi-Mandanten-Systeme, jede Konstellation mit konsistenten Produktfamilien.
Vorteil
Neue Familien ohne Änderung an Bestandscode ergänzbar, vollständig mockbare Fabriken für Tests.
Grenze
Nicht für einzelne, unabhängige Objekte einsetzen, dafür genügt eine einfache Factory-Methode.