Fabryka

Wzorzec projektowy fabryka

@ Marys_fotos

Fabryka to jeden z częściej wykorzystywanych wzorców projektowych. Wzorzec ten występuje w dwóch wariantach: metoda fabryczna (ang. Factory Method) i fabryka abstrakcyjna (ang. Abstract Factory). Celem zastosowania wzorca jest dostarczenie interfejsu do tworzenia rodzin powiązanych lub wzajemnie zależnych obiektów bez definiowania ich konkretnych klas.

Oprócz wymienionych wyżej wariantów fabryk istnieje również inna odmiana fabryki. Jest nią prosta fabryka (ang. Simple Factory), która nie jest pełnoprawnym wzorcem. Mnogość odmian sprawia, że są one często ze sobą mylone i błędnie opisywane.

Dlatego w dzisiejszym artykule opiszę wszystkie z odmian wraz konkretnymi przykładami. Dowiemy się jak dany wzorzec jest zbudowany oraz kiedy używać określonej odmiany wzorca.

Prosta fabryka

Rozważmy przykład producenta różnych marek samochodów. Poniższa klasa CarManufacture tworzy samochody w metodzie manufactureCar. Samochody te implementują interfejs Car. Metoda ta posiada jeden parametr wejściowy o nazwie type. Określa on markę tworzonego samochodu. W obecnej wersji można utworzyć samochody trzech marek. Po utworzeniu samochód poddawany on jest testom. W tym celu wywoływane są metody start, accelerate oraz stop.

public class CarManufacture {

    public void manufactureCar(String type) {
        Car car;
        if (type.equals("Audi")) {
            car = new Audi();
        } else if (type.equals("Volvo")) {
            car = new Volvo();
        } else if (type.equals("Ferrari")) {
            car = new Ferrari();
        } else {
            throw new IllegalArgumentException("Unknown car." + type);
        }

        car.start();
        car.accelerate();
        car.stop();
    }
}

Niestety kod powyższej metody nie jest zamknięty na na modyfikacje. Jeśli pojawi się potrzeba dodania nowej marki samochodu to metodę trzeba będzie zmienić. W ten sposób zostanie naruszona zasada otwarte-zamknięte.

Zwróćmy jeszcze uwagę na obiekty przypisywane do zmiennej car. Tworzymy tutaj obiekty za pomocą operatora new. Co to oznacza? Oznacza to bezpośrednią zależność klasy CarManufacture od tych klas. Takich zależności powinniśmy unikać. Zmiana w importowanej klasie może wymusić zmiany w klasie CarManufacture.

Co powinniśmy w tej sytuacji zrobić?

Musimy z powyższej metody wydzielić kod, który ulega zmianom. Tym kodem jest fragment tworzący poszczególne obiekty samochodów. Kod ten umieścimy w osobnej klasie. Klasa ta będzie miała tylko jedną odpowiedzialność. Będzie nią tworzenie obiektów określonych marek samochodów. W ten sposób utworzymy prostą fabrykę samochodów.

Przykładowa implementacja prostej fabryki

Spójrzmy jak będzie wyglądała implementacja prostej fabryki tworzącej poszczególne marki samochodów.

public class SimpleFactory {

    public Car createCar(String type) {
        if (type.equals("Audi")) {
            return new Audi();
        } else if (type.equals("Volvo")) {
            return new Volvo();
        } else if (type.equals("Ferrari")) {
            return new Ferrari();
        } else {
            throw new IllegalArgumentException("Unknown car." + type);
        }
    }
}

Metoda createCar fabryki przyjmuje jeden parametr, którym jest typ samochodu. Na jego podstawie zwracana jest instancja odpowiedniej klasy reprezentującej daną markę samochodu. Po utworzeniu prostej fabryki aktualizujemy kod klasy CarManufacture.

public class CarManufacture {

    private SimpleFactory factory;

    public CarManufacture(SimpleFactory factory) {
        this.factory = factory;
    }

    public void manufactureCar(String type) {
        Car car = factory.createCar(type);
        car.start();
        car.accelerate();
        car.stop();
    }
}

Obiekt fabryki przekazywany jest jako argument konstruktora klasy CarManufacture. W metodzie manufactureCar wywołujemy metodę createCar fabryki. Jako argument wejściowy przekazujemy do niej typ samochodu. Fabryka zwraca obiekt klasy określonej marki samochodu.

Dzięki zastosowaniu fabryki usunęliśmy zależności od klas reprezentujących poszczególne marki samochodów. Zostały one ukryte w fabryce.

Tworzenie obiektów samochodów w fabryce przynosi nam jeszcze jedną korzyść. Jeśli inna klasa będzie chciała utworzyć samochód to użyje gotowej fabryki. Kod odpowiedzialny za tworzenie samochodów nie będzie powielany w wielu miejscach. W ten sposób jesteśmy zgodni z zasadą DRY (ang. Don’t Repeat Yourself, pol. Nie powtarzaj się). Operacja tworzenia obiektu będzie zatem oddzielona od miejsca jego użycia.

Mimo korzyści jakie powyżej opisano prosta fabryka nie jest pełnoprawnym wzorcem. Nie została ona ujęta przez Gang of Four (GoF) w słynnej książce Wzorce projektowe. Elementy oprogramowania obiektowego wielokrotnego użytku.

Prosta fabryka statyczna

Prosta fabryka może występować również w postaci metody statycznej (ang. static factory). Wtedy to metoda, która tworzy obiekty jest metodą statyczną. W przypadku naszego przykładu z fabryką samochodów taka metoda wyglądała by następująco:

public static Car createCar(String type) {
    if (type.equals("Audi")) {
        return new Audi();
    } else if (type.equals("Volvo")) {
        return new Volvo();
    } else if (type.equals("Ferrari")) {
        return new Ferrari();
    } else {
        throw new IllegalArgumentException("Unknown car." + type);
    }
}

Rozwiązanie to posiada zarówno wady i zalety. Wadą jest to, że nie można użyć dziedziczenia i zmieniać działania konstruktora klasy. Natomiast zaletą takiego rozwiązania jest brak konieczności tworzenia obiektu fabryki.

Metoda fabrykująca

Prosta fabryka pozwoliła zamknąć proces tworzenia obiektów w jednej klasie. Pozwoliła też usunąć zależność między klasą CarManufacture i tworzonymi w fabryce obiektami. Jednak prosta fabryka w pewnym sytuacjach posiada pewne ograniczenia.

Wróćmy do naszego przykładu producenta różnych marek samochodów. Producent ten zdecydował się uruchomić produkcję samochodów w USA. Samochody produkowane na rynek amerykański będą się różniły od tych produkowanych w Europie. Musimy więc zapewnić tworzenie amerykańskich wersji tych samych samochodów. Prosta fabryka nie jest rozwiązaniem, które w prosty sposób zapewni tworzenie obu wersji samochodów. Dodatkowo, klasa CarManufacture nadal posiada zależność do klasy fabryki. A fabryka ta jest konkretną implementacją. Czyli mamy tu zależność od konkretnej implementacji, a nie od elementu abstrakcyjnego.

Z pomocą przychodzi nam wzorzec metody fabrykującej zwany też metodą wytwórczą. Definicja metody fabrykującej opisana przez Bandę Czworga brzmi następująco:

Metoda fabrykująca definiuje interfejs do tworzenia obiektu, ale pozwala podklasom zdecydować, jakiej klasy obiekt zostanie utworzony. Metoda fabrykująca przekazuje odpowiedzialność za tworzenie obiektów klasom podrzędnym.

Aby zastosować wzorzec metody fabrykującej należy zmienić klasę CarManufacture. Do tej pory klasa ta była odpowiedzialna za tworzenie obiektów poszczególnych samochodów. Teraz stanie się ona klasą abstrakcyjną. Będzie ona reprezentować ogólną fabrykę. Dziedziczące po niej klasy będą reprezentować fabryki samochodów na konkretny rynek. Powstanie w ten sposób hierarchia klas, którą przedstawia poniższy diagram.

Klasy podrzędne dla abstrakcyjnej fabryki z metodą fabrykującą.

Abstrakcyjna klasa CarManufacture będzie posiadać “metodę fabrykującą” createCar. Jej zadaniem będzie tworzenie obiektów. Będzie ona zadeklarowana jako metoda abstrakcyjna. Dzięki temu odpowiedzialności za tworzenia obiektów zostanie przeniesiona do podklas. W podklasach będą tworzone obiekty konkretnych samochodów. W ten sposób powstanie druga hierarchia klas, którą przedstawia poniższy diagram.

Klasy podrzędne dla abstrakcyjnej fabryki z metodą fabrykującą.

Przykładowa implementacja metody fabrykującej

Implementację klasy CarManufacture uwzględniającą powyższe zmiany przedstawia poniższy kod.

package pl.javadeveloper.design.patterns.factory.factorymethod;

import pl.javadeveloper.design.patterns.factory.model.Car;

public abstract class CarManufacture {

    public void manufactureCar(String type) {
        Car car = createCar(type);
        car.start();
        car.accelerate();
        car.stop();
    }

    protected abstract Car createCar(String type);
}

Podklasa powyższej klasy odpowiedzialna za produkcję samochodów na rynek europejski będzie wyglądała następująco:

public class EuropeanCarManufacture extends CarManufacture {

    protected Car createCar(String type) {
        if (type.equals("Audi")) {
            return new EuropeanAudi();
        } else if (type.equals("Volvo")) {
            return new EuropeanVolvo();
        } else if (type.equals("Ferrari")) {
            return new EuropeanFerrari();
        } else {
            throw new IllegalArgumentException("Unknown car." + type);
        }
    }
}

Natomiast klasa fabryki produkująca samochody na rynek amerykański będzie wyglądała następująco:

public class AmericanCarManufacture extends CarManufacture {

    protected Car createCar(String type) {
        if (type.equals("Audi")) {
            return new AmericanAudi();
        } else if (type.equals("Volvo")) {
            return new AmericanVolvo();
        } else if (type.equals("Ferrari")) {
            return new AmericanFerrari();
        } else {
            throw new IllegalArgumentException("Unknown car." + type);
        }
    }
}

Spójrzmy na dwie poglądowe implementacje klas konkretnych samochodów, które implementują interfejs Car. Pozostałe klasy reprezentujące poszczególne implementacje samochodów będą wyglądały podobnie.

public class EuropeanFerrari implements Car {
    public void start() {
        System.out.println("European Ferrari started.");
    }

    public void accelerate() {
        System.out.println("European Ferrari accelerated.");
    }

    public void stop() {
        System.out.println("European Ferrari stopped.");
    }
}
public class AmericanAudi implements Car {

    public void start() {
        System.out.println("American Audi started.");
    }

    public void accelerate() {
        System.out.println("American Audi accelerated.");
    }

    public void stop() {
        System.out.println("American Audi stopped.");
    }
}

Poniższy przykład pokazuje w jaki sposób można użyć powyższych fabryk.

public class Client {

    public static void main(String[] args) {
        CarManufacture manufacture = new EuropeanCarManufacture();
        manufacture.manufactureCar("Ferrari");

        manufacture = new AmericanCarManufacture();
        manufacture.manufactureCar("Audi");
    }

}

W wyniku wykonania powyższego programu na konsoli zostaną wypisane następujące komunikaty:

European Ferrari started.
European Ferrari accelerated.
European Ferrari stopped.
American Audi started.
American Audi accelerated.
American Audi stopped.

Diagram klas metody fabrykującej w języku UML

Rzućmy jeszcze okiem na poniższy diagram klas wzorca metody fabrykującej.

Klasy podrzędne dla abstrakcyjnej fabryki z metodą fabrykującą.

W lewym górnym rogu diagramu widzimy abstrakcyjny typ bazowy dla wszystkich produktów. W naszym przykładzie z samochodami typem tym był interfejs Car.

W lewym dolnym rogu diagramu znajduje się konkretny produkt tworzony przez fabrykę. W naszych rozważaniach produktami tymi były konkretne samochody. Wszystkie były klasami, które implementowały interfejs Car.

W prawym górnym rogu znajduje się abstrakcyjny typ bazowy reprezentujący fabrykę. Fabryka będąca interfejsem lub klasą abstrakcyjną posiada metodę, za pomocą której tworzone są obiekty. Metoda ta nazywana jest metodą fabrykującą - stąd też pochodzi nazwa wzorca. Jest ona metodą abstrakcyjną, która musi zostać zaimplementować przez wszystkie podklasy.

W prawym dolnym rogu znajduje się konkretna implementacja fabryki. To ona tworzy tworzy konkretne obiekty. W naszych rozważaniach tymi produktami były konkretne samochody.

Fabryka abstrakcyjna

Ostatnią z omawiany odmian fabryk jest fabryka abstrakcyjna. Celem tego wzorca jest stworzenie interfejsu do tworzenia rodzin powiązanych ze sobą obiektów. Jako przykład takiej rodziny produktów można podać części samochodowe. W jednej fabryce mogą być produkowane na przykład następujące części:

  • silniki,
  • światła,
  • opony.

Podobnie jak w przypadku metody fabrykującej konstrukcja fabryki abstrakcyjnej bazuje na abstrakcji. Jednak w przeciwieństwie do metody fabrykującej fabryka abstrakcyjna tworzy grupę obiektów. Najczęściej typy tworzonych przez fabrykę abstrakcyjną obiektów są różne. W przypadku metody fabrycznej tworzony był tylko jeden typ obiektu. W naszych rozważaniach dla metody fabrykującej typem tym był rodzaj samochodu.

Przykładowa implementacja fabryki abstrakcyjnej

Wróćmy ponownie do branży motoryzacyjnej i stwórzmy fabrykę abstrakcyjną. Jej celem będzie tworzenie części samochodowych takich jak silniki, światła oraz opony. Będziemy więc za jej pomocą tworzyć trzy rodzaje obiektów. Oto przykładowa implementacja interfejsu reprezentująca taką fabrykę.

public interface CarEquipmentFactory {
    Engine createEngine();
    Light createLight();
    Tire createTire();
}

Powyższy interfejs zawiera deklarację trzech metod. Każda z metod jest odpowiedzialna z tworzenie jednego rodzaju obiektu. Engine, Light oraz Tire są typami abstrakcyjnymi. Jak widzimy zarówno fabryka jak i tworzone przez nią produkty są typami abstrakcyjnymi. Dzięki temu klasa, która będzie używać fabryki będzie uzależniona od abstrakcji, a nie od konkretnej implementacji.

Spójrzmy na uproszczoną definicję typów tworzonych przez fabrykę.

public interface Engine {
    void produceEngine();
}
public interface Light {
    void produceLight();
}
public interface Tire {
    void produceTire();
}

Dla celów poglądowych każdy z nich ma zadeklarowaną tylko jedną prostą metodę.

Mamy już zaprojektowaną fabrykę abstrakcyjną oraz zwracane przez nią produkty. Pora stworzyć konkretne implementacje. Zarówno dla samej fabryki, jak i dla konkretnych produktów. Załóżmy, że chcemy mieć dwie fabryki. Pierwsza z nich będzie produkować części do zwykłych samochodów. Natomiast druga do samochodów o podwyższonym komforcie. Oto jak wygląda implementacja fabryki abstrakcyjnej, która produkuje części do zwykłych samochodów.

public class EconomyCarEquipmentFactory implements CarEquipmentFactory {

    public Engine createEngine() {
        return new PetrolEngine();
    }

    public Light createLight() {
        return new Halogen();
    }

    public Tire createTire() {
        return new BudgetTire();
    }
}

Widzimy, że każda z metod odpowiedzialna jest za tworzenie konkretnych obiektów (produktów). Uproszczona implementacja każdego z tworzonych produktów przedstawia się następująco:

public class PetrolEngine implements Engine {

    public void produceEngine() {
        System.out.println("Producing petrol engine.");
    }
}
public class Halogen implements Light {

    public void produceLight() {
        System.out.println("Producing halogen light.");
    }
}
public class BudgetTire implements Tire {

    public void produceTire() {
        System.out.println("Producing budget tire.");
    }
}

Spójrzmy jeszcze na drugą z implementacji fabryki abstrakcyjnej. Będzie ona tworzyła części do samochodów o wyższym standardzie.

public class ComfortCarEquipmentFactory implements CarEquipmentFactory {

    public Engine createEngine() {
        return new HybridEngine();
    }

    public Light createLight() {
        return new LedLight();
    }

    public Tire createTire() {
        return new PremiumTire();
    }
}

Uproszczona implementacja konkretnych części z powyższej fabryki będzie wyglądała następująco:

public class HybridEngine implements Engine {

    public void produceEngine() {
        System.out.println("Producing hybrid engine.");
    }
}
public class LedLight implements Light {

    public void produceLight() {
        System.out.println("Producing led light.");
    }
}
public class PremiumTire implements Tire {

    public void produceTire() {
        System.out.println("Producing premium tire.");
    }
}

Mamy już gotowe implementację fabryk oraz tworzonych przez nich produktów. Spójrzmy jeszcze na prosty przykład, który pokazuje ich użycie.

public class Client {

    public static void main(String[] args) {
        createEquipment(new EconomyCarEquipmentFactory());
        createEquipment(new ComfortCarEquipmentFactory());
    }

    private static void createEquipment(CarEquipmentFactory carEquipmentFactory) {
        Engine engine = carEquipmentFactory.createEngine();
        engine.produceEngine();

        Light light = carEquipmentFactory.createLight();
        light.produceLight();

        Tire tire = carEquipmentFactory.createTire();
        tire.produceTire();
    }
}

Powyższy program oprócz metody main zawiera metodę createEquipment(). Metoda ta posiada jeden parametr. Jest nim obiekt implementujący zdefiniowany przez nas interfejs CarEquipmentFactory. Dzięki temu, że jest on typem abstrakcyjnym możemy dostarczać do metody różne implementacje fabryki. Wewnątrz metody po kolei wywoływane są metody odpowiedzialne za tworzenie określonych części. W metodzie main metoda createEquipment() jest wywoływana dwukrotnie. W pierwszym wywołaniu przekazujemy jej obiekty klasy EconomyCarEquipmentFactory. Natomiast w drugim przekazywanym obiektem jest obiekt klasy ComfortCarEquipmentFactory.

W wyniku wykonania powyższego programu na konsoli zobaczymy następującej wyniki:

Producing petrol engine.
Producing halogen light.
Producing budget tire.
Producing hybrid engine.
Producing led light.
Producing premium tire.

Diagram klas wzorca fabryki abstrakcyjnej w języku UML

Diagram klas fabryki abstrakcyjnej jest bardziej złożony niż diagram metody fabrykującej. Przedstawia się on jak na poniższym rysunku. Diagram klas wzorca projektowego abstract factory (fabryka abstrakcyjna).

U samej góry diagramu znajduje się interfejs reprezentujący fabrykę abstrakcyjną. Posiada on metody, które muszą zostać zaimplementowane. Na diagramie pokazane zostały dwie poglądowe metody. Oczywiście ilość metod w tym interfejsie jest zależna od ilości rodzajów produktów, które mają być tworzone.

Poniżej znajdują się dwie fabryki, które są konkretnymi implementacjami. Każda z nich tworzy inny rodzaj produktów. Produkty te implementują interfejsy ProduktAbstrakcyjnyA i ProduktAbstrakcyjnyB.

Na samym dole diagramu znajdują się po dwa konkretne produkty tworzone przez każdą z fabryk. Pierwsza z fabryk o nazwie FabrykaKonkretna1 tworzy produkty ProduktA1 i ProduktB1. Natomiast fabryka FabrykaKonkretna2 tworzy produkty ProduktA2 i ProduktB2.

Zalety i wady fabryk

Prosta fabryka

Zalety:

  1. Usunięcie zależności między miejscem utworzenia obiektu, a jego użyciem.
  2. Ukrycie procesu tworzenia obiektów w fabryce.

Wady:

  1. Naruszenie zasady Open-Close Principle w przypadku rozbudowy fabryki.
  2. Utrudnione testowanie klasy fabryki.

Metoda fabrykująca

Zalety:

  1. Zgodność z zasadą Open-Close Principle w przypadku dodania nowych typów produktów.
  2. Ukrycie procesu tworzenia obiektów w fabrykach konkretnych.
  3. Usunięcie zależności klienta od konkretnych klas (zarówno od klas fabryk i obiektów tworzonych przez te fabryki)
  4. Łatwiejsze testowanie fabryk konkretnych.

Wady:

  1. Większa złożoność kodu.

Fabryka abstrakcyjna

Zalety:

  1. Klient używający fabryki nie jest zależny od konkretnej implementacji. Posługuje się interfejsami zarówno dla fabryki jak i tworzonych przez nią produktów.
  2. Możliwość łatwej podmiany całych grup produktów przez zmianę fabryki konkretnej.

Wady:

  1. Większa złożoność kodu.
  2. Naruszenie zasady Open-Close Principle w przypadku rozbudowy fabryki o nowe typy produktów.

Przykłady fabryk w JDK

Metoda fabrykująca:

Fabryka abstrakcyjna:

Materiały dodatkowe

Książki

Jeśli chcesz dowiedzieć się więcej na temat fabryk to polecam zapoznanie się z następującymi książkami:

  1. Wzorce projektowe. Elementy oprogramowania obiektowego wielokrotnego użytku. Autorzy: Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides.

  2. Wzorce projektowe. Rusz głową! Autorzy: Eric Freeman, Bert Bates, Kathy Sierra, Elisabeth Robson.

  3. Java EE. Zaawansowane wzorce projektowe. Autorzy: Murat Yener, Alex Theedom.

  4. Zwinne wytwarzanie oprogramowania. Najlepsze zasady, wzorce i praktyki. Autor: Robert C. Martin

  5. Refaktoryzacja do wzorców projektowych. Autor: Joshua Kerievsky.

Materiały wideo

Christopher Okhravi prezentuje wzorzec metody fabrykującej

Derek Banas przedstawia wzorzec fabryki abstrakcyjnej

Podsumowanie

Jak wspomniałem na wstępie wzorce fabryk należą do jednych z najbardziej popularnych wzorców. Spowodowane to jest tym, ze fabryki są bardzo użytecznym rozwiązaniem. Wszystkie opisane fabryki ukrywają przed klientami proces tworzenia obiektów.

Prosta fabryka nie jest pełnoprawnym wzorcem projektowym. Za to jest łatwa w zrozumieniu oraz implementacji.

Metoda fabrykująca oraz fabryka abstrakcyjna wprowadza elementy abstrakcyjne. Dzięki temu zmniejszamy zależności w kodzie programu. Abstrakcja sprawia, że kod jest mniej wrażliwy na zmiany i łatwiejszy w rozbudowie. W przypadku metody fabrykującej używamy dziedziczenia. Proces tworzenie obiektów odbywa się w podklasach. Natomiast w przypadku fabryki abstrakcyjnej posługujemy się kompozycją. Proces tworzenia obiektów w przypadku fabryki abstrakcyjnej odbywa się w konkretnych fabrykach.

Kody źródłowe opisane w tym artykule znajdują się na GitHub-ie.

Przeczytaj także