Wzorzec Projektowy Factory: Elastyczne Tworzenie Obiektów

Wzorzec fabryczny należy do grupy wzorców kreacyjnych, które koncentrują się na procesie tworzenia obiektów. Jego głównym celem jest zapewnienie elastyczności w tworzeniu instancji obiektów, umożliwiając odroczenie decyzji o tym, która klasa zostanie użyta, do podklas lub konkretnych implementacji. Dzięki temu kod klienta pozostaje niezależny od szczegółów implementacji produktów, co ułatwia rozszerzanie systemu i jego utrzymanie.

Kiedy Warto Użyć Wzorca Factory?

Wzorzec Factory jest szczególnie przydatny, gdy:

  • Proces tworzenia obiektu jest skomplikowany lub zależy od dynamicznych parametrów.
  • Potrzebujemy dekoplowania kodu klienta od konkretnych klas produktów.
  • System musi obsługiwać różne typy obiektów w sposób polimorficzny.
  • Chcemy przestrzegać zasady otwarte/zamknięte, umożliwiając dodawanie nowych typów bez modyfikacji istniejącego kodu.

Przykładowo, w grach komputerowych Factory może służyć do tworzenia różnych typów jednostek, w zależności od kontekstu, bez zmiany kodu klienta.

Schematy Implementacji Wzorca Factory

Wzorzec Factory można wdrożyć na dwa główne sposoby: jako Metodę Fabrykującą (Factory Method), gdzie abstrakcyjna metoda decyduje o typie obiektu, oraz jako Fabrykę Abstrakcyjną (Abstract Factory), która tworzy rodziny powiązanych obiektów.

Metoda Fabrykująca (Factory Method)

W tej implementacji definiuje się interfejs do tworzenia pojedynczego typu obiektu, ale podklasy decydują o konkretnej klasie. Kluczowa jest abstrakcyjna metoda fabrykująca, implementowana w konkretnych fabrykach. Wzorzec promuje zasadę otwarte/zamknięte, pozwalając na dodawanie nowych typów bez zmian w kodzie klienta. Jest przydatny, gdy tworzenie obiektu zależy od dynamicznych parametrów, np. typu płatności w systemie e-commerce.

Struktura Wzorca w Kodzie

Struktura obejmuje abstrakcyjną fabrykę z metodą create, konkretną fabrykę implementującą tę metodę (często z użyciem switch lub enuma), abstrakcyjny produkt definiujący interfejs oraz konkretne produkty. Klient używa fabryki bez znajomości szczegółów.

Przykład Kodu: Tworzenie Metod Płatności

Własny przykład w Java: fabryka tworzy obiekty płatności (CreditCardPayment, PayPalPayment) na podstawie enuma PaymentType.

Java
// Abstrakcyjna fabryka
abstract class PaymentFactory {
    abstract Payment createPayment(PaymentType type);
}

// Konkretna fabryka
public class ConcretePaymentFactory extends PaymentFactory {
    @Override
    public Payment createPayment(PaymentType type) {
        switch (type) {
            case CREDIT_CARD:
                return new CreditCardPayment("Visa", 1000.0);
            case PAYPAL:
                return new PayPalPayment("example@email.com", 500.0);
            default:
                throw new UnsupportedOperationException("Nieznany typ płatności");
        }
    }
}

// Abstrakcyjny produkt
public abstract class Payment {
    private String details;
    private double amount;

    protected Payment(String details, double amount) {
        this.details = details;
        this.amount = amount;
    }

    public String getDetails() { return details; }
    public double getAmount() { return amount; }

    public abstract void process();
}

// Konkretne produkty
public class CreditCardPayment extends Payment {
    public CreditCardPayment(String cardType, double amount) {
        super("Karta: " + cardType, amount);
    }

    @Override
    public void process() {
        System.out.println("Przetwarzanie płatności kartą: " + getAmount());
    }
}

public class PayPalPayment extends Payment {
    public PayPalPayment(String email, double amount) {
        super("Email: " + email, amount);
    }

    @Override
    public void process() {
        System.out.println("Przetwarzanie płatności PayPal: " + getAmount());
    }
}

// Enum
public enum PaymentType {
    CREDIT_CARD, PAYPAL;
}

// Klient
public class Main {
    public static void main(String[] args) {
        PaymentFactory factory = new ConcretePaymentFactory();
        Payment cardPayment = factory.createPayment(PaymentType.CREDIT_CARD);
        cardPayment.process();  // Wyjście: Przetwarzanie płatności kartą: 1000.0

        Payment paypalPayment = factory.createPayment(PaymentType.PAYPAL);
        paypalPayment.process();  // Wyjście: Przetwarzanie płatności PayPal: 500.0
    }
}

Analiza i Wyjaśnienie Działania

  • Abstrakcyjna fabryka (PaymentFactory): Definiuje metodę createPayment, abstrakcyjną, umożliwiając klientowi tworzenie płatności bez znajomości konkretnych klas.
  • Konkretna fabryka (ConcretePaymentFactory): Implementuje createPayment z użyciem switch na podstawie PaymentType, tworząc odpowiednie obiekty z predefiniowanymi parametrami.
  • Abstrakcyjny produkt (Payment): Definiuje interfejs z polami details, amount oraz abstrakcyjną metodą process().
  • Konkretne produkty (CreditCardPayment, PayPalPayment): Rozszerzają Payment, implementując specyficzne zachowanie.
  • Enum PaymentType: Zapewnia bezpieczne określanie typu.
  • Klient (Main): Używa fabryki do tworzenia obiektów, operując na abstrakcyjnym typie Payment.

Analiza: Dekoplowanie pozwala na łatwe dodawanie nowych typów płatności bez zmian w kliencie. Brak walidacji parametrów (np. ujemna kwota) to potencjalny problem.

Zalety

  • Dekoplowanie i elastyczność: Klient nie zna konkretnych klas, co ułatwia rozszerzanie i testowanie.
  • Polimorfizm: Obiekty traktowane jednolicie.
  • Bezpieczeństwo typów: Enum minimalizuje błędy.

Wady i Potencjalne Problemy

  • Zwiększona złożoność: Więcej klas i kodu.
  • Prostota switch: Przy wielu typach staje się nieporęczny.
  • Brak walidacji: Może prowadzić do błędów runtime.

Fabryka Abstrakcyjna (Abstract Factory)

Ta wersja umożliwia tworzenie rodzin powiązanych obiektów bez określania konkretnych klas. Jest bardziej złożona, zapewniając spójność między obiektami, np. w systemach z różnymi motywami GUI. Idealna dla scenariuszy wymagających kompatybilnych grup obiektów.

Struktura Wzorca w Kodzie

Obejmuje abstrakcyjną fabrykę z metodami dla każdej rodziny, konkretne fabryki tworzące spójne produkty, abstrakcyjne produkty dla rodzin oraz konkretne implementacje.

Przykład Kodu: Tworzenie Elementów GUI

Własny przykład: fabryki tworzą przyciski i pola wyboru dla systemów Windows i Mac.

Java
// Abstrakcyjna fabryka
abstract class GUIFactory {
    abstract Button createButton();
    abstract Checkbox createCheckbox();
}

// Konkretne fabryki
public class WindowsFactory extends GUIFactory {
    @Override
    public Button createButton() {
        return new WindowsButton("Windows Button");
    }

    @Override
    public Checkbox createCheckbox() {
        return new WindowsCheckbox("Windows Checkbox");
    }
}

public class MacFactory extends GUIFactory {
    @Override
    public Button createButton() {
        return new MacButton("Mac Button");
    }

    @Override
    public Checkbox createCheckbox() {
        return new MacCheckbox("Mac Checkbox");
    }
}

// Abstrakcyjne produkty
public abstract class Button {
    private String label;

    protected Button(String label) {
        this.label = label;
    }

    public String getLabel() { return label; }
    public abstract void render();
}

public abstract class Checkbox {
    private String label;

    protected Checkbox(String label) {
        this.label = label;
    }

    public String getLabel() { return label; }
    public abstract void render();
}

// Konkretne produkty
public class WindowsButton extends Button {
    public WindowsButton(String label) {
        super(label);
    }

    @Override
    public void render() {
        System.out.println("Renderowanie przycisku Windows: " + getLabel());
    }
}

public class MacButton extends Button {
    public MacButton(String label) {
        super(label);
    }

    @Override
    public void render() {
        System.out.println("Renderowanie przycisku Mac: " + getLabel());
    }
}

public class WindowsCheckbox extends Checkbox {
    public WindowsCheckbox(String label) {
        super(label);
    }

    @Override
    public void render() {
        System.out.println("Renderowanie checkbox Windows: " + getLabel());
    }
}

public class MacCheckbox extends Checkbox {
    public MacCheckbox(String label) {
        super(label);
    }

    @Override
    public void render() {
        System.out.println("Renderowanie checkbox Mac: " + getLabel());
    }
}

// Klient
public class Main {
    public static void main(String[] args) {
        GUIFactory windowsFactory = new WindowsFactory();
        Button winButton = windowsFactory.createButton();
        Checkbox winCheckbox = windowsFactory.createCheckbox();
        winButton.render();  // Wyjście: Renderowanie przycisku Windows: Windows Button
        winCheckbox.render();  // Wyjście: Renderowanie checkbox Windows: Windows Checkbox

        GUIFactory macFactory = new MacFactory();
        Button macButton = macFactory.createButton();
        Checkbox macCheckbox = macFactory.createCheckbox();
        macButton.render();  // Wyjście: Renderowanie przycisku Mac: Mac Button
        macCheckbox.render();  // Wyjście: Renderowanie checkbox Mac: Mac Checkbox
    }
}

Analiza i Wyjaśnienie Działania

  • Abstrakcyjna fabryka (GUIFactory): Definiuje metody dla rodzin (Button, Checkbox).
  • Konkretne fabryki (WindowsFactory, MacFactory): Tworzą spójne elementy GUI dla platformy.
  • Abstrakcyjne produkty (Button, Checkbox): Definiują interfejsy dla rodzin.
  • Konkretne produkty: Implementują specyficzne renderowanie.
  • Klient (Main): Używa fabryk do tworzenia kompatybilnych zestawów.

Analiza: Zapewnia spójność, ale dodanie nowego elementu GUI wymaga zmian we wszystkich fabrykach.

Zalety

  • Spójność rodzin: Produkty z jednej fabryki są kompatybilne.
  • Dekoplowanie i elastyczność: Łatwe dodawanie nowych rodzin.

Wady i Potencjalne Problemy

  • Zwiększona złożoność: Wiele abstrakcji i klas.
  • Trudności w rozszerzaniu: Dodanie nowego produktu wymaga modyfikacji wszystkich fabryk.
  • Overkill dla prostych systemów: Może komplikować kod niepotrzebnie.

Rozszerzanie Systemu

Aby dodać nową platformę (np. Linux), utwórz LinuxFactory z odpowiednimi produktami. Dla nowego elementu (np. Slider), dodaj metodę do GUIFactory i zaimplementuj w każdej fabryce.

Zalety i Wady Wzorca Factory

Zalety

  • Centralizacja tworzenia: Ułatwia zarządzanie obiektami.
  • Rozszerzalność: Dodawanie nowych typów bez zmian w kliencie.
  • Separacja odpowiedzialności: Kod tworzenia oddzielony od użycia.

Wady

  • Złożoność: Więcej klas i abstrakcji.
  • Wydajność: Może wprowadzać overhead w prostych przypadkach.

Przykłady Zastosowania w Świecie Rzeczywistym

  • Gry komputerowe: Tworzenie jednostek lub broni w zależności od poziomu.
  • Systemy płatności: Fabryka do różnych metod płatności.
  • Frameworki GUI: Tworzenie elementów dla różnych platform (np. Look and Feel w Swing).
  • Cloud services: Fabryki do usług AWS, Azure itp.
  • Produkcja napojów: Symulacja fabryk lemoniady z różnymi składnikami.

Te przykłady ilustrują praktyczne zastosowanie Factory w elastycznym tworzeniu obiektów.

Wnioski dla Wzorca Factory

Wzorzec Factory zapewnia elastyczność w tworzeniu obiektów, dekoplowanie i łatwe rozszerzanie systemów. Metoda Fabrykująca nadaje się do pojedynczych typów, a Fabryka Abstrakcyjna do rodzin produktów. Zalety jak centralizacja przeważają nad wadami jak zwiększona złożoność w złożonych aplikacjach. Łączy się dobrze z innymi wzorcami, np. Strategy, dla dynamicznych zachowań.