Abstract Factory in PHP umsetzen: Familien verwandter Objekte sauber erzeugen
AI generated
<?php
8.4
PHP · OOP · Design Patterns · Kreationsmuster
Abstract Factory in PHP umsetzen
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.

20 Min. Lesezeit Abstract Factory · Interfaces · Dependency Injection PHP 8.x

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.

11. FAQ: Abstract Factory in PHP

1Unterschied zu Factory Method?
Factory Method erzeugt ein einzelnes Objekt, Abstract Factory eine ganze Familie zueinander passender Objekte über ein Interface mit mehreren Methoden.
2Wann einsetzen?
Wenn mehrere Objekte zusammenpassen müssen und niemals gemischt werden dürfen, etwa Client und Validator eines Zahlungsanbieters.
3Neue Produkttypen schwieriger?
Ja, ein neuer Produkttyp erfordert Anpassungen in allen konkreten Fabriken. Neue Produktfamilien hinzuzufügen bleibt dagegen einfach.
4Wie testet man das?
Mit einer Fake-Fabrik-Implementierung, die Test-Doubles zurückgibt. Der Client-Code bleibt unverändert, nur die Fabrik-Instanz wird ausgetauscht.
5Kompatibel mit DI-Containern?
Sehr gut. Bindung vom Fabrik-Interface auf konkrete Implementierung im Container registrieren, Client fordert einfach das Interface an.
6Häufigster Fehler?
Einsatz ohne echte Produktfamilie, also für ein einzelnes Objekt. Dafür reicht eine einfache Factory-Methode völlig aus.
7Für Multi-Mandanten geeignet?
Ja, jeder Mandant erhält eine eigene konkrete Fabrik, oft kombiniert mit einer Registry, die anhand einer Mandanten-ID auflöst.
8switch statt Abstract Factory schlecht?
Ja, jede neue Familie erfordert eine Änderung an der zentralen switch-Stelle. Abstract Factory verteilt diese Verantwortung auf separate Klassen.
9Darf eine Fabrik Zustand halten?
Konfigurationswerte wie API-Keys ja, veränderlichen, zwischen Aufrufen geteilten Zustand besser nicht. Dafür eignen sich andere Muster.
10Wann Builder statt Abstract Factory?
Wenn ein einzelnes, komplexes Objekt schrittweise mit vielen optionalen Parametern konstruiert werden soll, statt mehrere Objekte konsistent zu erzeugen.