Wzorzec projektowy – Fabryka

1.1 Definicja.
 

Ogólnie rzecz ujmując wzorce z rodziny wzorców fabrykujących wykorzystywane są w celu delegowania odpowiedzialności za tworzenie innych obiektów do innych klas, tak aby użytkownik mógł wywołać tylko jedna metodę i w konsekwencji otrzymywał gotowe obiekty.

Istnieje kilka odmian wzorców z rodziny Fabryka, my skupimy się na najbardziej popularnym:

  • Metoda fabrykująca (factory method)

Metoda fabrykująca to wzorzec projektowy, który pozwala na tworzenie w zorganizowany sposób różnych obiektów implementujących ten sam interfejs. Tak więc to nie my bezpośrednio tworzymy obiekt za pomocą słowa kluczowego new, tylko zgłaszamy się do fabryki z żądaniem wyprodukowania go zgodnie z naszymi oczekiwaniami. Fabryka musi dostarczyć nam metodę wytwarzającą (fabrykującą) obiekt, ale to, w jaki sposób go konstruuje, pozostaje dla nas niewidoczne.

Uzupełniając, możemy przedstawić inną odmianę wzorca Factory mianowicie Fabryka abstrakcyjna. Koncepcja jest taka sama, różnica polega na tym, że dzięki wprowadzeniu abstrakcji możemy wytworzyć obiekt dowolnego typu a nie jak w przypadku metody wytwórczej tylko konkretnego typu. Wybór konkretnej implementacji będzie oczywiście zależał od złożoności i potrzeb końcowych naszego projektu. 

2.0 Architektura wzorca (factory method).
  • Product – klasa bazowa dla obiektów tworzonych przez metodę wytwórczą,
  • Creator– klasa zawierająca metodę wytwórczą factoryMethod,
  • SublassedProduct – przykładowa podklasa Product,
  • SubclassedCreator – podklasa, nadpisująca metodę wytwórczą zwracając instancję SubclassedProduct.

Product może być zdefiniowany jako interfejs. W takim przypadku podklasy Creator tworzą instancje różnych klas implementujących interfejs Product. Metoda factoryMethod wcale nie musi być abstrakcyjna. Klasa Creator może mieć domyślną implementację tej metody, która może być napisana przez podklasy.

3.0 Przykład implementacji

Niech za przykład posłuży nam tytuł znanego filmu Fabryka czekolady. 

Napiszemy zatem prostą implementację wzorca factory method w oparciu o fabrykę produkującą kilka rodzajów wyrobów czekoladowych.

Poniżej znajduje się implementacja bez wykorzystania wzorca factory method 

Zacznijmy od utworzenia klasy enum z rodzajami czekolady:

public enum RodzajCzekolady {

    MLECZNA,
    GORZKA,
    ZORZECHAMI
}

Następnie tworzymy klasę abstrakcyjną Czekolada:

public abstract class Czekolada {

    private RodzajCzekolady rodzajCzekolady;

    public Czekolada(RodzajCzekolady rodzajCzekolady) {
        this.rodzajCzekolady = rodzajCzekolady;
    }

    public RodzajCzekolady getRodzajCzekolady() {
        return rodzajCzekolady;
    }
}

Widzimy powyżej, że w klasie abstrakcyjnej Czekolada, utworzyliśmy jedno prywatne pole z rodzajem czekolady, mamy też konstruktor do którego przekazujemy rodzaj czekolady oraz zwykłego gettera zwracającego rodzaj czekolady.

Czas na utworzenie klasy o nazwie TabliczkaCzekolady rozszerzającej klasę Czekolada:

public class TabliczkaCzekolady extends Czekolada{

    public TabliczkaCzekolady(RodzajCzekolady rodzajCzekolady) {
        super(rodzajCzekolady);
    }
}

Nie pozostaje nam zatem nic innego jak utworzyć klasę Main w której powołamy do życia obiekty tabliczkaCzekolady oraz baton (implementacja klasy Baton jest analogiczna jak TabliczkiCzekolady więc ją pominąłem):

public class Main {

    public static void main(String[] args) {

        Czekolada baton = new Baton(RodzajCzekolady.MLECZNA);
        System.out.println("Baton, rodzaj czekolady: " + baton.getRodzajCzekolady());

        Czekolada tabliczkaczekolady = new TabliczkaCzekolady(RodzajCzekolady.GORZKA);
        System.out.println("Tabliczka czekolady, rodzaj czekolady: " + tabliczkaczekolady.getRodzajCzekolady());

    }
}

Mamy więc powyżej przedstawione podejście klasyczne jeśli chodzi o tworzenie obiektów w naszej Fabryce Czekolady. Zacznijmy zatem  wykorzystywać wzorzec Factory method:

Wykorzystanie wzorca Factory Method - implementacja:

Znamy już naszego enuma z rodzajami czekolady:

public enum RodzajCzekolady {
    MLECZNA,
    GORZKA,
    ZORZECHAMI
}

Dołóżmy jeszcze klasę enum z rodzajami gotowych wyrobów czekoladowych:

public enum RodzajWyrobuGotowego {
    BATON,
    TABLICZKA_CZEKOLADY
}

Utworzymy teraz abstrakcyjną klasę Czekolada, jednak tym razem konstruktor tej klasy nie będzie już publiczny. Przypominamy sobie, że chcemy delegować tworzenie obiektów do innej klasy, zapobiegamy zatem aby użytkownik (programista) nie mógł łatwo tworzyć obiektów w tradycyjny sposób bez delegowania tej czynności do metody fabrykującej. Temu właśnie służy modyfikator dostępu protected. Musimy jeszcze nadać domyślny dostęp naszym konkretnym klasom wyrobów (usuwamy modyfikator dostępu w klasach Baton i Tabliczka Czekolady) i to już koniec, obecnie nie możemy już utworzyć obiektu czekolady w naszej klasie Main za pomocą konstruktora, obiekty te mogą być tworzone tylko i wyłącznie w naszej fabryce czekolady!

public abstract class Czekolada {

    private RodzajCzekolady rodzajCzekolady;
    
    protected Czekolada(RodzajCzekolady rodzajCzekolady) {
        this.rodzajCzekolady = rodzajCzekolady;
    }

    public RodzajCzekolady getRodzajCzekolady() {
        return rodzajCzekolady;
    }
}

Podobnie jak wczesniej, tworzymy klasę produktu, w naszym przypadku jest to jak pamiętamy np. TabliczkaCzekolady. która rozszerza klasę Czekolada. 

public class TabliczkaCzekolady extends Czekolada {

    TabliczkaCzekolady(RodzajCzekolady rodzajCzekolady) {
        super(rodzajCzekolady);
    }
}
Nadszedł czas na Creatora czyli klasę Factory

Przechodzimy do utworzenia klasy abstrakcyjnej Factory, która jak wskazano na schemacie UML powyżej posiadać będzie jedną metodę kreacyjną, my nazwiemy ją createCzekolada. Zwróćmy uwagę, że metoda ta jest również abstrakcyjna.

public abstract class Factory {

    abstract public Czekolada createCzekolada(RodzajWyrobuGotowego rodzajWyrobuGotowego);
}

Teraz możemy zająć się najważniejszym czyli utworzeniem naszej klasy FabrykaCzekolady, która będzie odpowiedzialna za tworzenie naszych obiektów. Zwróćmy uwagę, na kluczowy fragment, to w tym bloku kodu zwracany jest nowy obiekt z użyciem słowa kluczowego new.

public class FabrykaCzekolady extends Factory{

    @Override
    public Czekolada createCzekolada(RodzajWyrobuGotowego rodzajWyrobuGotowego) {

        switch (rodzajWyrobuGotowego) {
            case BATON:
                return new Baton(RodzajCzekolady.MLECZNA);
            case TABLICZKA_CZEKOLADY:
                return new TabliczkaCzekolady(RodzajCzekolady.GORZKA);
            default:
                throw new UnsupportedOperationException("No such type");
        }
    }
}

Zobaczmy jak teraz wyglądać będzie klasa Main:

public class Main {

    public static void main(String[] args) {

       Factory factory = new FabrykaCzekolady();
       Czekolada baton = factory.createCzekolada(RodzajWyrobuGotowego.BATON);

    }
}

Prawda, że ładnie?. Tworzymy obiekt fabryki a następnie nasz obiekt, delegując tą operację do obiektu factory a konkretnie do metody fabrykującej createCzekolada i to wszystko!

4.0 Podsumowanie


Wzorce projektowe z grupy fabrykujących są bardzo często spotykane w praktyce programistycznej. Ich główną zaletą jest fakt, że enkapsulujemy (hermetyzujemy) tworzenie nowych obiektów czyli użytkownik, który tworzy obiekty za pomocą fabryki nie wie jak wygląda konkretnie tworzenie tych obiektów, jest to ukryte przed użytkownikiem. Kolejnym ważnym elementem jest fakt, że używamy abstrakcji. Podczas tworzenia fabryki używamy klasy abstrakcyjnej a nie konkretnej klasy. Dzięki wykorzystaniu abstrakcji możemy później łatwiej rozszerzać naszą fabrykę o kolejne typy obiektów.

Leave a Comment

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