Abstract Factory pattern – Tworzenie rodzin obiektów w Javie

Abstract Factory pozwala tworzyć rodziny powiązanych obiektów bez określania ich konkretnych klas. Zamiast new Button() używasz factory.createButton(). Dzięki temu kod jest niezależny od konkretnych implementacji i łatwy do rozszerzania.

Wyobraź sobie że tworzysz aplikację która musi działać na różnych platformach – Windows, Mac, Linux. Każda platforma ma swoje przyciski, okna, menu. Jak stworzyć kod który będzie działał wszędzie, ale używał właściwych komponentów dla każdej platformy?

Dlaczego to ważne

Tworzenie obiektów przez new tworzy sztywne powiązania między kodem a konkretnymi klasami. Abstract Factory rozwiązuje ten problem tworząc fabryki które produkują całe rodziny powiązanych obiektów. Kod staje się elastyczny i łatwy do rozszerzania o nowe warianty.

Co się nauczysz:

  • Jak działa wzorzec Abstract Factory w praktyce
  • Kiedy używać go zamiast prostego Factory Method
  • Jak zaimplementować fabryki dla różnych produktów
  • Jakie problemy rozwiązuje w prawdziwych aplikacjach
  • Jak łączy się z innymi wzorcami projektowymi

Wymagania wstępne:

Podstawy Javy, znajomość interfejsów i dziedziczenia. Pomocna będzie wiedza o wzorcu Factory Method, ale nie jest konieczna.

Problem z tworzeniem obiektów

Załóżmy że tworzysz aplikację z interfejsem użytkownika. Bez wzorców projektowych kod wyglądałby tak:

public class Application {
    public void createUI() {
        // Sztywne powiązania z konkretnymi klasami!
        Button button = new WindowsButton();
        Checkbox checkbox = new WindowsCheckbox();
        TextBox textbox = new WindowsTextBox();
        
        // Co jeśli chcemy Mac UI?
        // Musimy zmieniać kod w wielu miejscach...
    }
}
Pułapka: Gdy użyjesz new bezpośrednio, twój kod staje się zależny od konkretnych klas. Każda zmiana wymaga modyfikacji w wielu miejscach.

Czym jest wzorzec Abstract Factory

Abstract Factory to wzorzec projektowy który pozwala tworzyć rodziny powiązanych obiektów bez określania ich konkretnych klas. Definiuje interfejs do tworzenia grup produktów które współpracują ze sobą.

Abstract Factory to jak fabryka mebli. Masz fabrykę nowoczesnych mebli (stolik + krzesło + szafa w stylu modern) i fabrykę klasycznych mebli (stolik + krzesło + szafa w stylu classic). Każda fabryka produkuje kompletny zestaw pasujących do siebie mebli.

Podstawowa struktura składa się z kilku elementów:

// 1. Interfejsy produktów
public interface Button {
    void render();
    void onClick();
}

public interface Checkbox {
    void render();
    void toggle();
}

// 2. Konkretne produkty Windows
public class WindowsButton implements Button {
    @Override
    public void render() {
        System.out.println("Render Windows style button");
    }
    
    @Override
    public void onClick() {
        System.out.println("Windows button clicked");
    }
}

public class WindowsCheckbox implements Checkbox {
    @Override
    public void render() {
        System.out.println("Render Windows style checkbox");
    }
    
    @Override
    public void toggle() {
        System.out.println("Windows checkbox toggled");
    }
}

// 3. Konkretne produkty Mac
public class MacButton implements Button {
    @Override
    public void render() {
        System.out.println("Render Mac style button");
    }
    
    @Override
    public void onClick() {
        System.out.println("Mac button clicked");
    }
}

public class MacCheckbox implements Checkbox {
    @Override
    public void render() {
        System.out.println("Render Mac style checkbox");
    }
    
    @Override
    public void toggle() {
        System.out.println("Mac checkbox toggled");
    }
}

Tworzenie Abstract Factory

Teraz tworzymy fabryki które będą produkować rodziny komponentów:

// 4. Interfejs Abstract Factory
public interface GUIFactory {
    Button createButton();
    Checkbox createCheckbox();
}

// 5. Konkretna fabryka Windows
public class WindowsFactory implements GUIFactory {
    @Override
    public Button createButton() {
        return new WindowsButton();
    }
    
    @Override
    public Checkbox createCheckbox() {
        return new WindowsCheckbox();
    }
}

// 6. Konkretna fabryka Mac
public class MacFactory implements GUIFactory {
    @Override
    public Button createButton() {
        return new MacButton();
    }
    
    @Override
    public Checkbox createCheckbox() {
        return new MacCheckbox();
    }
}
Każda fabryka tworzy kompletną rodzinę produktów. WindowsFactory zawsze utworzy komponenty w stylu Windows, MacFactory – w stylu Mac.

Użycie wzorca w aplikacji

Teraz nasza aplikacja może używać różnych fabryk bez zmiany kodu biznesowego:

public class Application {
    private GUIFactory factory;
    private Button button;
    private Checkbox checkbox;
    
    public Application(GUIFactory factory) {
        this.factory = factory;
    }
    
    public void createUI() {
        // Kod niezależny od konkretnych implementacji!
        button = factory.createButton();
        checkbox = factory.createCheckbox();
    }
    
    public void render() {
        button.render();
        checkbox.render();
    }
}

// Użycie
public class Main {
    public static void main(String[] args) {
        GUIFactory factory;
        
        String osName = System.getProperty("os.name").toLowerCase();
        if (osName.contains("windows")) {
            factory = new WindowsFactory();
        } else if (osName.contains("mac")) {
            factory = new MacFactory();
        } else {
            factory = new WindowsFactory(); // domyślnie
        }
        
        Application app = new Application(factory);
        app.createUI();
        app.render();
    }
}
Pro tip: Aplikacja nie wie jakie konkretne obiekty dostaje. Wie tylko że button i checkbox będą do siebie pasować bo pochodzą z tej samej fabryki.

Rozszerzanie o nowe platformy

Dodanie nowej platformy (np. Linux) wymaga tylko stworzenia nowych klas:

// Nowe produkty Linux
public class LinuxButton implements Button {
    @Override
    public void render() {
        System.out.println("Render Linux style button");
    }
    
    @Override
    public void onClick() {
        System.out.println("Linux button clicked");
    }
}

public class LinuxCheckbox implements Checkbox {
    @Override
    public void render() {
        System.out.println("Render Linux style checkbox");
    }
    
    @Override
    public void toggle() {
        System.out.println("Linux checkbox toggled");
    }
}

// Nowa fabryka Linux
public class LinuxFactory implements GUIFactory {
    @Override
    public Button createButton() {
        return new LinuxButton();
    }
    
    @Override
    public Checkbox createCheckbox() {
        return new LinuxCheckbox();
    }
}
Typowy błąd: Dodawanie metod do interfejsu fabryki łamie istniejące implementacje. Lepiej tworzyć nowe interfejsy dziedziczące z głównego.

Kiedy używać Abstract Factory

Używaj gdy:

  • Masz rodziny powiązanych produktów (Windows UI, Mac UI, Linux UI)
  • Chcesz gwarantować że produkty z jednej rodziny współpracują
  • Kod powinien być niezależny od sposobu tworzenia obiektów
  • Planisz dodawać nowe warianty produktów

NIE używaj gdy:

  • Masz tylko jeden typ produktu (wystarczy Factory Method)
  • Produkty nie muszą ze sobą współpracować
  • Nie przewidujesz dodawania nowych wariantów

Przykład z życia – System płatności

Praktyczny przykład to system płatności z różnymi dostawcami:

// Produkty
public interface PaymentProcessor {
    void processPayment(BigDecimal amount);
}

public interface PaymentValidator {
    boolean validate(PaymentData data);
}

// Fabryka PayPal
public class PayPalFactory implements PaymentFactory {
    @Override
    public PaymentProcessor createProcessor() {
        return new PayPalProcessor();
    }
    
    @Override
    public PaymentValidator createValidator() {
        return new PayPalValidator();
    }
}

// Fabryka Stripe
public class StripeFactory implements PaymentFactory {
    @Override
    public PaymentProcessor createProcessor() {
        return new StripeProcessor();
    }
    
    @Override
    public PaymentValidator createValidator() {
        return new StripeValidator();
    }
}

Zalety i wady wzorca

ZaletyWady
Gwarantuje spójność produktówZwiększa złożoność kodu
Łatwe dodawanie nowych rodzinTrudne dodawanie nowych produktów
Separacja kodu od implementacjiWięcej klas do utrzymania
Przestrzega Open/Closed PrincipleMoże być over-engineering
Jaka jest różnica między Factory Method a Abstract Factory?

Factory Method tworzy jeden typ produktu, Abstract Factory tworzy rodziny powiązanych produktów. Jeśli masz tylko Button, użyj Factory Method. Jeśli masz Button + Checkbox + Menu jako zestaw, użyj Abstract Factory.

Czy Abstract Factory to singleton?

Niekoniecznie. Fabryki mogą być singletonami jeśli nie przechowują stanu, ale to nie jest wymaganie wzorca. Ważniejsze jest że produkują spójne rodziny obiektów.

Jak dodać nowy produkt do istniejących fabryk?

To główna wada wzorca. Musisz zmodyfikować interfejs fabryki i wszystkie implementacje. Lepiej przewidzieć to z góry lub używać innych wzorców jak Builder.

Czy można łączyć Abstract Factory z Dependency Injection?

Tak, świetnie się uzupełniają. DI container może wstrzykiwać odpowiednią fabrykę na podstawie konfiguracji. W Spring można użyć @Qualifier do wyboru konkretnej fabryki.

Kiedy Abstract Factory staje się zbyt skomplikowany?

Gdy masz więcej niż 3-4 typy produktów lub więcej niż 5-6 rodzin. Wtedy lepiej rozważyć podział na mniejsze fabryki lub inne wzorce jak Prototype czy Builder.

Przydatne zasoby:

🚀 Zadanie dla Ciebie

Stwórz system tworzenia dokumentów z fabrykame PDF i Word. Każda fabryka powinna tworzyć Document, Header i Footer. Zaimplementuj PDFFactory i WordFactory, a następnie napisz kod który utworzy dokument nie wiedząc z jakiej fabryki pochodzi.

Czy używasz wzorca Abstract Factory w swoich projektach? Jakie napotkałeś wyzwania przy jego implementacji? Podziel się doświadczeniem w komentarzach!

Zostaw komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Przewijanie do góry