Factory, Adapter, Strategy, Visitor: vier Muster im Detail

Factory, Adapter, Strategy, Visitor: vier Muster im Detail

Factory, Adapter, Strategy, Visitor: vier Muster im Detail

Artikel-Übersicht

Vier Entwurfsmuster im Detail, alle nach demselben Schema: das Problem, der Code, wann es sich lohnt, und ob eine moderne Sprache das Problem nicht schon löst. Die Auswahl ist nicht willkürlich — es sind die vier, nach denen einzeln gesucht wird. Bei Factory trenne ich die zwei Varianten, die im Netz durchgängig verwechselt werden. Bei Adapter grenze ich das Muster gegen die gleichnamige Architekturschicht ab, die etwas anderes meint. Strategy bekommt den direkten Vergleich zwischen Klassenform und Funktionsparameter, weil das die häufigste offene Frage ist. Und Visitor bekommt eine ehrliche Einordnung: Er wird selten gebraucht, aber wo er passt, gibt es keine gute Alternative. Alle Beispiele in Java, durchgehend dieselbe Sprache — der Vergleich zwischen den Mustern wäre sonst wertlos.

Die Einordnung aller 23 Muster mit Kategorien und Alltagsrelevanz steht in Entwurfsmuster: welche man kennt, welche man braucht.

Factory: Objekte erzeugen, ohne die Klasse zu kennen

Das Problem

Ein Code, der ein Objekt braucht, muss normalerweise wissen, welche konkrete Klasse er erzeugt. Damit hängt er an dieser Klasse. Soll später eine andere Implementierung verwendet werden, muss jede new-Stelle geändert werden.

Die Factory verlagert die Entscheidung an einen Ort.

Factory Method und Abstract Factory sind nicht dasselbe

Diese Verwechslung ist im Netz die Regel und lohnt zwei Sätze:

Factory Method ist eine Methode, die ein Objekt erzeugt und in Unterklassen überschrieben werden kann. Eine Klasse, ein Produkt.

Abstract Factory ist eine Schnittstelle mit mehreren Erzeugungsmethoden, die zusammengehörige Produkte erzeugen. Eine Fabrik, eine ganze Produktfamilie.

Im Code

// Factory Method: die Unterklasse entscheidet, was erzeugt wird
abstract class Berichtsgenerator {
    abstract Bericht erzeugeBericht();      // die Fabrikmethode

    void ausgeben() {
        Bericht b = erzeugeBericht();
        b.schreiben();
    }
}

class PdfGenerator extends Berichtsgenerator {
    Bericht erzeugeBericht() { return new PdfBericht(); }
}
// Abstract Factory: eine ganze Familie zusammengehöriger Produkte
interface Oberflaechenfabrik {
    Schaltflaeche schaltflaeche();
    Textfeld textfeld();
}

class DunkleFabrik implements Oberflaechenfabrik {
    public Schaltflaeche schaltflaeche() { return new DunkleSchaltflaeche(); }
    public Textfeld textfeld()          { return new DunklesTextfeld(); }
}

Der Unterschied wird an der zweiten Klasse sichtbar: Die Abstract Factory garantiert, dass Schaltfläche und Textfeld zusammenpassen — man kann nicht versehentlich ein helles Textfeld in eine dunkle Oberfläche setzen. Genau das ist ihr Zweck.

Wann es sich lohnt — und wann eine Funktion reicht

Die Factory Method ist in Sprachen mit Funktionen erster Klasse meist eine Funktion. Statt einer abstrakten Klasse mit überschriebener Erzeugungsmethode übergibt man einen Erzeuger als Parameter:

class Berichtsgenerator {
    private final Supplier<Bericht> erzeuger;

    Berichtsgenerator(Supplier<Bericht> erzeuger) { this.erzeuger = erzeuger; }

    void ausgeben() { erzeuger.get().schreiben(); }
}

new Berichtsgenerator(PdfBericht::new).ausgeben();

Weniger Code, keine Vererbungshierarchie, dieselbe Wirkung. Diese Form ist in modernen Codebasen der Normalfall — auch wenn sie selten so genannt wird.

Abstract Factory bleibt relevant, wenn tatsächlich Produktfamilien wechseln. Der Fall ist seltener, aber wenn er auftritt, gibt es keine leichtere Lösung.

In Domain-Driven Design gibt es außerdem den Begriff Factory für die Erzeugung von Aggregaten mit fachlichen Regeln — das ist verwandt, aber nicht dasselbe Muster. Mehr dazu in Domain-Driven Design in der Praxis.

Adapter: zwei Schnittstellen zusammenbringen

Das Problem

Du hast eine Klasse, die genau das tut, was du brauchst — aber mit der falschen Schnittstelle. Sie ändern kannst du nicht, weil sie aus einer Fremdbibliothek stammt.

Der Adapter übersetzt.

Im Code

// Das ist, was dein Code erwartet
interface Zahlungsdienst {
    Ergebnis belasten(Betrag betrag, Kundennummer kunde);
}

// Das ist, was die Fremdbibliothek anbietet
class ExternesGateway {
    public GatewayResponse charge(long cents, String customerRef, String currency) { ... }
}

// Der Adapter dazwischen
class GatewayAdapter implements Zahlungsdienst {
    private final ExternesGateway gateway;

    GatewayAdapter(ExternesGateway gateway) { this.gateway = gateway; }

    public Ergebnis belasten(Betrag betrag, Kundennummer kunde) {
        GatewayResponse antwort = gateway.charge(
            betrag.inCent(), kunde.alsText(), betrag.waehrung().code());
        return antwort.isSuccess() ? Ergebnis.erfolg() : Ergebnis.fehler(antwort.getError());
    }
}

Der Adapter macht drei Dinge: Er übersetzt die Signatur, er übersetzt die Datentypen, und er übersetzt das Fehlermodell. Der dritte Punkt ist der wichtigste und wird am häufigsten vergessen — eine Fremdbibliothek, deren Ausnahmen durch deinen Code wandern, ist nicht gekapselt.

Objekt-Adapter oder Klassen-Adapter

Der obige ist ein Objekt-Adapter: Er hält das fremde Objekt als Feld. Der Klassen-Adapter erbt stattdessen von der fremden Klasse und implementiert gleichzeitig die eigene Schnittstelle. Das setzt Mehrfachvererbung oder Schnittstellen voraus und bindet fester.

Die Empfehlung ist eindeutig: Objekt-Adapter. Er funktioniert in jeder Sprache, koppelt loser und lässt sich testen, indem man ein anderes Objekt hineingibt.

Wann es sich lohnt

Der Alltagsfall ist immer derselbe: eine Fremdbibliothek hinter der eigenen Schnittstelle kapseln. Der Nutzen zeigt sich erst später — wenn der Anbieter gewechselt wird, wenn die Bibliothek eine inkompatible Hauptversion bekommt, oder wenn man für Tests eine Attrappe braucht.

Ein Hinweis zur Begriffsverwirrung: In der Architektur heißt eine ganze Schicht „Adapter" — die äußere Schicht, die zwischen Anwendungskern und Außenwelt übersetzt. Das ist derselbe Gedanke auf anderer Ebene, aber nicht dasselbe Muster. Die Architekturebene beschreibe ich in Clean Architecture.

Strategy: Verhalten austauschbar machen

Das Problem

Eine Fallunterscheidung, die wächst. Erst zwei Fälle, dann fünf, dann acht — und jeder neue Fall bedeutet, dieselbe Methode wieder anzufassen.

BigDecimal versandkosten(Bestellung b) {
    if (b.land() == DE) return new BigDecimal("4.90");
    else if (b.land() == AT) return new BigDecimal("9.90");
    else if (b.land() == CH) return new BigDecimal("19.90");
    // ... und so weiter
}

Strategy zieht jeden Fall in ein eigenes Objekt.

Im Code

interface Versandkostenberechnung {
    BigDecimal berechne(Bestellung bestellung);
}

class InlandVersand implements Versandkostenberechnung {
    public BigDecimal berechne(Bestellung b) { return new BigDecimal("4.90"); }
}

class SchweizVersand implements Versandkostenberechnung {
    public BigDecimal berechne(Bestellung b) {
        return new BigDecimal("19.90").add(zollpauschale(b));
    }
}

// Der Kontext kennt nur die Schnittstelle
class Bestellabschluss {
    private final Map<Land, Versandkostenberechnung> strategien;

    BigDecimal versandkosten(Bestellung b) {
        return strategien.get(b.land()).berechne(b);
    }
}

Ein neuer Fall bedeutet jetzt eine neue Klasse und einen Eintrag in der Zuordnung — die bestehende Logik wird nicht angefasst.

Strategy als Funktion

Die häufigste offene Frage: Braucht es dafür Klassen?

Map<Land, Function<Bestellung, BigDecimal>> strategien = Map.of(
    DE, b -> new BigDecimal("4.90"),
    CH, b -> new BigDecimal("19.90").add(zollpauschale(b))
);

Kürzer, und für einfache Fälle die bessere Wahl. Die Klassenform bleibt besser, wenn die Strategie einen eigenen Zustand hat, wenn sie mehrere Methoden braucht, wenn sie einen sprechenden Namen verdient, oder wenn sie einzeln getestet werden soll. Eine Lambda-Funktion, die zwanzig Zeilen lang wird, gehört in eine Klasse.

Wann es sich lohnt

Strategy ist das Muster mit der höchsten Alltagsrelevanz der vier. Der Auslöser ist immer derselbe: eine Fallunterscheidung, die wächst. Bei zwei Fällen ist ein if klarer. Ab vier lohnt der Umbau fast immer.

Ein Anwendungsfall aus der Praxis: der Zugriff auf ein Sprachmodell. Anbieter und Modelle wechseln derzeit im Halbjahrestakt — eine Strategie je Anbieter hinter einer gemeinsamen Schnittstelle macht daraus eine Konfigurationsfrage statt einer Umbaumaßnahme. Wie unterschiedlich sich die Anbieter verhalten, steht im Vergleich der führenden LLM-Anbieter, die lokale Variante in Ollama: lokale Sprachmodelle betreiben.

Visitor: Operationen von der Struktur trennen

Das Problem

Du hast eine Objektstruktur mit vielen verschiedenen Typen — etwa einen Syntaxbaum oder ein Dokumentmodell — und willst darauf immer neue Operationen ausführen: ausgeben, prüfen, umwandeln, zählen.

Ohne Visitor bedeutet jede neue Operation eine neue Methode in jedem Typ. Bei zwanzig Typen und fünf Operationen sind das hundert Methoden, verteilt über zwanzig Klassen.

Im Code

interface Knoten {
    <T> T akzeptiere(Besucher<T> besucher);
}

class Zahl implements Knoten {
    final int wert;
    public <T> T akzeptiere(Besucher<T> b) { return b.besucheZahl(this); }
}

class Summe implements Knoten {
    final Knoten links, rechts;
    public <T> T akzeptiere(Besucher<T> b) { return b.besucheSumme(this); }
}

interface Besucher<T> {
    T besucheZahl(Zahl zahl);
    T besucheSumme(Summe summe);
}

// Eine Operation: auswerten
class Auswerter implements Besucher<Integer> {
    public Integer besucheZahl(Zahl z)   { return z.wert; }
    public Integer besucheSumme(Summe s) {
        return s.links.akzeptiere(this) + s.rechts.akzeptiere(this);
    }
}

// Noch eine Operation: ausgeben — ohne die Knotenklassen anzufassen
class Ausgeber implements Besucher<String> {
    public String besucheZahl(Zahl z)   { return String.valueOf(z.wert); }
    public String besucheSumme(Summe s) {
        return "(" + s.links.akzeptiere(this) + " + " + s.rechts.akzeptiere(this) + ")";
    }
}

Der Mechanismus heißt Double Dispatch: Der erste Aufruf (akzeptiere) wählt anhand des Knotentyps, der zweite (besucheZahl) anhand des Besuchertyps. Zwei Typen bestimmen gemeinsam, welcher Code läuft.

Warum er so selten gebraucht wird

Visitor löst ein Problem, das in Sprachen mit Musterabgleich gar nicht entsteht. Wo man über den Typ direkt verzweigen kann, braucht es die Konstruktion nicht:

// Musterabgleich statt Visitor
static int auswerten(Knoten k) {
    return switch (k) {
        case Zahl z  -> z.wert;
        case Summe s -> auswerten(s.links) + auswerten(s.rechts);
    };
}

Deutlich kürzer und leichter zu lesen. Der Preis ist, dass die Vollständigkeitsprüfung nur funktioniert, wenn die Typhierarchie geschlossen ist — genau dafür gibt es versiegelte Typen.

Und der zweite Grund für die Seltenheit: Visitor macht das Hinzufügen von Operationen leicht und das Hinzufügen von Typen schwer. Ein neuer Knotentyp bedeutet eine neue Methode in jedem Besucher. Das ist genau die richtige Verteilung für Compiler und Dokumentmodelle, wo die Typen stabil sind und die Operationen wachsen — und die falsche für fast alles andere.

Wo er trotzdem die richtige Wahl ist

  • Syntaxbäume und Compiler. Die Knotentypen stehen fest, die Durchläufe werden mehr.
  • Dokumentmodelle. Ausgabe in verschiedene Formate ist genau der Fall.
  • Sprachen ohne Musterabgleich. Wo die Alternative fehlt, bleibt Visitor die sauberste Lösung.

Die vier im Vergleich

Muster Problem Alltag Moderne Alternative Bleibt relevant, weil …
Factory Method Erzeugung entkoppeln gering Erzeugerfunktion als Parameter … die Idee bleibt, nur die Form ändert sich
Abstract Factory zusammenpassende Produktfamilien mittel keine wirklich einfachere … die Zusammengehörigkeit sonst nicht garantiert ist
Adapter inkompatible Schnittstellen verbinden hoch keine … jede Fremdbibliothek eine Grenze braucht
Strategy wachsende Fallunterscheidung hoch Funktionsparameter für einfache Fälle … benannte Strategien mit Zustand sonst fehlen
Visitor Operationen von der Struktur trennen gering Musterabgleich … stabile Typen mit wachsenden Operationen es brauchen

Häufige Fragen

Was ist der Unterschied zwischen Factory Method und Abstract Factory? Factory Method erzeugt ein Produkt und wird in Unterklassen überschrieben. Abstract Factory ist eine Schnittstelle mit mehreren Erzeugungsmethoden für eine zusammengehörige Produktfamilie.

Wann nimmt man Adapter? Immer dann, wenn eine Fremdbibliothek hinter der eigenen Schnittstelle gekapselt werden soll — inklusive Übersetzung des Fehlermodells.

Strategy als Klasse oder als Funktion? Als Funktion, wenn die Strategie kurz und zustandslos ist. Als Klasse, wenn sie Zustand hat, mehrere Methoden braucht, einen sprechenden Namen verdient oder einzeln getestet werden soll.

Warum wird Visitor so selten benutzt? Weil Musterabgleich in modernen Sprachen dasselbe kürzer löst — und weil Visitor neue Operationen leicht, neue Typen aber schwer macht.

Was ist Double Dispatch? Der Mechanismus hinter Visitor: Zwei Typen bestimmen gemeinsam, welcher Code ausgeführt wird, über zwei aufeinanderfolgende Methodenaufrufe.

Zusammenfassung und praktische Tipps

Vier Muster, sehr unterschiedliche Alltagsrelevanz. Die wichtigsten Punkte:

  • Adapter und Strategy braucht man laufend, Factory und Visitor selten.
  • Factory Method ist in modernen Sprachen meist eine Funktion. Abstract Factory bleibt, wo Produktfamilien zusammenpassen müssen.
  • Der Adapter übersetzt drei Dinge: Signatur, Datentypen und Fehlermodell. Das dritte wird am häufigsten vergessen.
  • Strategy lohnt ab etwa vier Fällen. Darunter ist eine Fallunterscheidung klarer.
  • Visitor macht Operationen leicht und Typen schwer. Das passt für Compiler und Dokumentmodelle, sonst selten.
  • Musterabgleich ersetzt Visitor, wenn die Sprache ihn anbietet und die Typhierarchie geschlossen ist.

Mein Rat: Lern Adapter und Strategy so gut, dass du sie im Schlaf schreibst — sie tauchen in jedem Projekt auf. Factory und Visitor solltest du erkennen können, wenn sie dir begegnen, aber greif nicht danach, bevor du ihr Problem tatsächlich hast. Der häufigste Fehler bei diesen vier ist nicht, sie falsch zu benutzen, sondern sie zu früh zu benutzen.

Ressourcen