Factory Method und Abstract Factory in Java

1. Überblick

Factory Method und Abstract Factory gehören beide zur Gruppe der Creational Patterns, helfen dir beide, Objekte zu erstellen, ohne direkt die konkrete Klasse mit new zu instantiieren. Aber diese beiden Patterns lösen unterschiedliche Probleme, und es ist leicht, sie zu verwechseln, wenn man sie gerade erst lernt.

In diesem Artikel wirst du den Unterschied durch Beispiele sehen: PaymentMethod (Factory Method) und ein System zum Erstellen von UI nach Design (Abstract Factory). Nach dem Lesen wirst du wissen, wann du nur eine einfache Factory brauchst und wann du eine ganze “Fabrik für Fabriken” benötigst.

2. Was ist Factory Method?

Factory Method definiert eine Methode zum Erstellen von Objekten, lässt aber die Subclass entscheiden, welche konkrete Klasse erstellt wird. Vereinfacht gesagt: du hast eine Interface-Methode create(), jede Subclass überschreibt sie, um eine andere Implementierung zurückzugeben.

2.1. Problem, das gelöst werden muss

Angenommen, du hast ein Zahlungssystem mit mehreren Typen: CreditCardPayment, MomoPayment, BankTransferPayment. Wenn der Client-Code direkt mit new instantiiert:

public void processOrder(Order order, String type) {
    PaymentMethod payment;
    if (type.equals("CREDIT_CARD")) {
        payment = new CreditCardPayment();
    } else if (type.equals("MOMO")) {
        payment = new MomoPayment();
    } else {
        payment = new BankTransferPayment();
    }
    payment.pay(order.getAmount());
}

Jedes Mal, wenn du einen neuen Zahlungstyp hinzufügst, musst du dieses if-else überall ändern, wo es verwendet wird. Das ist genau der Code Smell, den Factory Method behebt.

2.2. Factory Method anwenden

public interface PaymentMethod {
    void pay(BigDecimal amount);
}

public class CreditCardPayment implements PaymentMethod {
    @Override
    public void pay(BigDecimal amount) {
        System.out.println("Zahlung " + amount + " per Kreditkarte");
    }
}

public class MomoPayment implements PaymentMethod {
    @Override
    public void pay(BigDecimal amount) {
        System.out.println("Zahlung " + amount + " über Momo");
    }
}

Erklärung: PaymentMethod ist die gemeinsame Schnittstelle, jede Implementierung kümmert sich nur um ihre eigene Zahlungslogik. Das ist das “Produkt” (product), das die Factory erstellt.

Danach kommt die abstrakte Klasse, die die Factory Method enthält:

public abstract class PaymentProcessor {

    // Factory Method – die Subclass entscheidet, welche Instanz erstellt werden soll
    protected abstract PaymentMethod createPaymentMethod();

    public void processOrder(Order order) {
        PaymentMethod payment = createPaymentMethod();
        payment.pay(order.getAmount());
    }
}

public class CreditCardProcessor extends PaymentProcessor {
    @Override
    protected PaymentMethod createPaymentMethod() {
        return new CreditCardPayment();
    }
}

public class MomoProcessor extends PaymentProcessor {
    @Override
    protected PaymentMethod createPaymentMethod() {
        return new MomoPayment();
    }
}
  • createPaymentMethod() ist die Factory Method: sie ist eine abstrakte Methode, die jede Subclass überschreibt, um den richtigen Typ PaymentMethod zurückzugeben, den sie benötigt.
  • processOrder() ist die gemeinsame “Vorlage”, die nicht wissen muss, welcher Zahlungstyp gerade erstellt wird.

Der Client benutzt es folgendermaßen:

PaymentProcessor processor = new MomoProcessor();
processor.processOrder(new Order(new BigDecimal("150000")));
Zahlung 150000 über Momo

2.3. Factory Method testen

class PaymentProcessorTest {

    @Test
    void shouldCreateCorrectPaymentMethod() {
        PaymentProcessor processor = new CreditCardProcessor();
        PaymentMethod method = processor.createPaymentMethod();

        assertThat(method).isInstanceOf(CreditCardPayment.class);
    }
}

Hinweis: Um createPaymentMethod() (protected) im Test aufzurufen, platziere die Test-Klasse im gleichen Paket wie PaymentProcessor, oder ändere die Methode zu public, wenn du von außen testen möchtest.

3. Was ist Abstract Factory?

Abstract Factory stellt eine Schnittstelle bereit, um eine Familie von zusammenhängenden Objekten zu erstellen, ohne die konkrete Klasse jedes Objekts angeben zu müssen. Im Gegensatz zu Factory Method, das nur einen Produkttyp erstellt, erstellt Abstract Factory mehrere Produkte, die zusammenpassen müssen.

3.1. Problem, das gelöst werden muss

Angenommen, du erstellst ein UI-Design für eine Anwendung: ein “Light”-Design und ein “Dark”-Design. Jedes Design braucht ein eigenes Button und Checkbox, und diese beiden müssen im Stil aufeinander abgestimmt sein – du kannst nicht LightButton mit DarkCheckbox vermischen.

public interface Button {
    void render();
}

public interface Checkbox {
    void render();
}

public class LightButton implements Button {
    @Override
    public void render() {
        System.out.println("Render button mit weißem Hintergrund, schwarzer Text");
    }
}

public class DarkButton implements Button {
    @Override
    public void render() {
        System.out.println("Render button mit schwarzem Hintergrund, weißer Text");
    }
}

public class LightCheckbox implements Checkbox {
    @Override
    public void render() {
        System.out.println("Render checkbox mit hellem grauem Rahmen");
    }
}

public class DarkCheckbox implements Checkbox {
    @Override
    public void render() {
        System.out.println("Render checkbox mit weißem Rahmen");
    }
}

3.2. Abstract Factory definieren

public interface UiComponentFactory {
    Button createButton();
    Checkbox createCheckbox();
}

public class LightThemeFactory implements UiComponentFactory {
    @Override
    public Button createButton() {
        return new LightButton();
    }

    @Override
    public Checkbox createCheckbox() {
        return new LightCheckbox();
    }
}

public class DarkThemeFactory implements UiComponentFactory {
    @Override
    public Button createButton() {
        return new DarkButton();
    }

    @Override
    public Checkbox createCheckbox() {
        return new DarkCheckbox();
    }
}

UiComponentFactory ist die Schnittstelle, die die gesamte Produktfamilie definiert (Button + Checkbox). Jede Implementierung (LightThemeFactory, DarkThemeFactory) stellt sicher, dass die erstellten Produkte immer zum gleichen Design passen.

3.3. Abstract Factory im Client verwenden

public class ThemedApplication {

    private final UiComponentFactory factory;

    public ThemedApplication(UiComponentFactory factory) {
        this.factory = factory;
    }

    public void renderUi() {
        Button button = factory.createButton();
        Checkbox checkbox = factory.createCheckbox();
        button.render();
        checkbox.render();
    }
}
ThemedApplication app = new ThemedApplication(new DarkThemeFactory());
app.renderUi();
Render button mit schwarzem Hintergrund, weißer Text
Render checkbox mit weißem Rahmen

ThemedApplication weiß nicht und braucht nicht zu wissen, welche konkrete Klasse gerade erstellt wird. Sie hängt nur von der Schnittstelle UiComponentFactory ab, daher reicht es, die injizierte Factory zu wechseln, um das Design zu ändern.

3.4. Abstract Factory testen

class ThemedApplicationTest {

    @Test
    void shouldCreateConsistentComponentsForDarkTheme() {
        UiComponentFactory factory = new DarkThemeFactory();

        Button button = factory.createButton();
        Checkbox checkbox = factory.createCheckbox();

        assertThat(button).isInstanceOf(DarkButton.class);
        assertThat(checkbox).isInstanceOf(DarkCheckbox.class);
    }
}

Dieser Test bestätigt die wichtigste Eigenschaft der Abstract Factory: Die Produkte, die von der gleichen Factory erstellt werden, gehören immer zur gleichen “Familie” und vermischen nie Light mit Dark.

4. Vergleich: Factory Method vs. Abstract Factory

Kriterium Factory Method Abstract Factory
Anzahl der Produkttypen 1 Produkttyp (PaymentMethod) Mehrere zusammenhängende Produkttypen (Button, Checkbox)
Mechanismus Eine Methode in der Subclass überschreiben Mehrere Factory Methods in eine Schnittstelle komponieren
Beziehung Vererbung Normalerweise Komposition (Factory-Injection)
Ziel Den Client von der Notwendigkeit befreien, die konkrete Klasse eines Objekts zu kennen Die Konsistenz zwischen mehreren zusammenhängenden Objekten sicherstellen

Einfach zu merken: Abstract Factory wird normalerweise anhand einer Reihe von Factory Methods implementiert. In dem obigen Beispiel sind createButton() und createCheckbox() in UiComponentFactory tatsächlich zwei Factory Methods.

4.1. Wann sollte man Factory Method verwenden?

  • Du musst nur einen Typ Objekt erstellen, aber die Erstellungslogik ist komplex oder variiert je nach Kontext.
  • Du arbeitest mit einer Klassenhierarchie und möchtest, dass die Subclass selbst entscheidet, welchen Typ von Unterobjekt sie verwendet (das Template Method Pattern geht oft mit diesem Pattern einher).

4.2. Wann sollte man Abstract Factory verwenden?

  • Du musst mehrere zusammenhängende Objekte erstellen und sicherstellen, dass sie kompatibel sind (gleiches Design, gleicher Datenbankbetreiber, gleiche Plattform …).
  • Das System kann mit neuen “Produktfamilien” erweitert werden (z. B. HighContrastThemeFactory hinzufügen), ohne den Client-Code zu ändern.

Hinweis: Verwende Abstract Factory nicht, wenn du nur einen Produkttyp hast – das würde eine überflüssige Interface-Schicht erzeugen. Eine einfache Factory Method ist ausreichend.

5. Integration mit Spring Boot

In Spring schreibst du Factory Method/Abstract Factory selten manuell nach der GoF-Struktur, da der Spring-Container schon die Rolle der Factory spielt. Das Verständnis der Idee hilft dir aber, Beans besser zu gestalten.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>
@Configuration
public class PaymentFactoryConfig {

    @Bean
    public Map<String, PaymentMethod> paymentMethods() {
        return Map.of(
            "CREDIT_CARD", new CreditCardPayment(),
            "MOMO", new MomoPayment()
        );
    }
}
@Service
public class PaymentService {

    private final Map<String, PaymentMethod> paymentMethods;

    public PaymentService(Map<String, PaymentMethod> paymentMethods) {
        this.paymentMethods = paymentMethods;
    }

    public void pay(String type, BigDecimal amount) {
        PaymentMethod method = paymentMethods.get(type);
        if (method == null) {
            throw new IllegalArgumentException("Zahlungstyp wird nicht unterstützt: " + type);
        }
        method.pay(amount);
    }
}

Hier spielt der Spring-Container die Rolle der echten “Fabrik”: Er erstellt vorher die PaymentMethod-Beans und der Client sucht sie nur nach Schlüssel. Dies ist eine praktische Variante von Factory Method in der Spring-Umgebung – du brauchst nicht mehr für jeden Zahlungstyp eine eigene XxxProcessor-Klasse zu schreiben.

6. Häufige Fehler

  • Factory Method mit statischer Factory-Methode verwechseln (wie Optional.of(), List.of()). Statische Factory-Methoden sind nur eine kompaktere Möglichkeit, Objekte zu erstellen als der Konstruktor, sie haben nichts mit dem Polymorphismus des GoF-Patterns zu tun.
  • Abstract Factory übermäßig nutzen, wenn du nur ein Produkt hast: Wenn du nur Button ohne Checkbox hast, ist Abstract Factory Over-Engineering.
  • Die Factory zu viel Geschäftslogik kennen lassen: Eine Factory sollte sich nur um die Objekterstellung kümmern, nicht um Bestellungsverarbeitung, Validierung usw.
  • Die Konsistenz in der Abstract Factory vergessen: Wenn du dem Client erlaubst, Button von Factory A und Checkbox von Factory B zu wählen, hast du den Hauptzweck des Patterns zerstört.

7. Fazit

Factory Method ist geeignet, wenn du die Erstellung eines Objekts an die Subclass delegieren möchtest, um Code nach dem Open/Closed-Prinzip leicht zu erweitern. Abstract Factory geht weiter: Sie stellt sicher, dass eine Familie zusammenhängender Objekte immer konsistent erstellt wird und vermeidet die Vermischung inkompatibler Komponenten.

In der Praxis mit Spring Boot schreibst du normalerweise nicht manuell nach der GoF-Struktur, sondern nutzt den DI-Container als Factory. Aber das Verständnis der Natur dieser beiden Patterns hilft dir, Bibliothekscode zu verstehen (z. B. DocumentBuilderFactory, ConnectionFactory in JDBC) und Module besser zu erweitern.

design-patternjavacreational-patternspring-bootclean-code