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... } }
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ą.
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(); } }
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(); } }
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(); } }
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
Zalety | Wady |
---|---|
Gwarantuje spójność produktów | Zwiększa złożoność kodu |
Łatwe dodawanie nowych rodzin | Trudne dodawanie nowych produktów |
Separacja kodu od implementacji | Więcej klas do utrzymania |
Przestrzega Open/Closed Principle | Może być over-engineering |
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.
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.
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.
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.
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:
- Oracle Java 8 Documentation
- Abstract Factory Pattern – Refactoring Guru
- Spring Framework Documentation
🚀 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!