Kurs Java

Metoda fabrykująca

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.

Załóżmy, że tworzymy aplikację przechowującą zestawy dokumentów (dokument, załącznik, notatka). Interesuje nas wygenerowanie szablonu dla każdego rodzaju dokumentu. Dzięki niemu użytkownik dowie się, w jaki sposób ma wprowadzać do niego dane (co powinien wpisać w konkretnych polach).

Uruchomienie fabryki dokumentów

Nie mamy jeszcze żadnej klasy, ale już teraz warto zobaczyć, do czego będziemy dążyć w implementacji. Funkcjonalność całej fabryki opierać się będzie na zestawie kilku klas, z których większość będzie niewidoczna w miejscu wywołania. Tak naprawdę interesują nas tutaj tylko dostęp do klas odpowiedzialnych za produkcję różnych rodzajów dokumentów (dokument podstawowy, załącznik, notatka).

Każda z tych klas (producentów) posiada metodę createItem, której wywołanie spowoduje utworzenie obiektu typu Item. Oczywiście każda z klas dostarczy obiekt zbudowany zgodnie z jej oryginalny projektem. I tak na przykład DocumentItemProducer stworzy obiekt DocumentItem, a AttachmentItemProducer zbuduje obiekt AttachmentItem.
Java CheckedException
Kod do skopiowania, który...nie skompiluje się na tym etapie:
public static void main(String[] args) {

    DocumentItemProducer documentItemProducer = new DocumentItemProducer();
    Item document = documentItemProducer.createItem();

    AttachmentItemProducer attachmentItemProducer = new AttachmentItemProducer();
    Item attachment = attachmentItemProducer.createItem();

    NoteItemProducer noteItemProducer = new NoteItemProducer();
    Item note = noteItemProducer.createItem();
}
Kod nie skompiluje się, ponieważ najpierw musimy zbudować klasy, których tutaj używamy.

Produkcja

Wszystkie klasy produkujące nasze obiekty implementują interfejs (może to być też klasa abstrakcyjna). Najlepiej nazwać go tak, aby kojarzył się z fabryką oraz z tym, jakie elementy ta fabryka wytwarza. Stąd nadaliśmy mu nazwę ItemFactory (fabryka tworząca różne rodzaje itemów).

Mamy trzy klasy, które implementują interfejs ItemFactory. Każda z nich robi to po swojemu. Producent dokumentów tworzy obiekt DocumentItem, producent załączników buduje obiekt AttachmentItem, a producent notatek tworzy NoteItem. Ważne jest to, że każda implementacja metody konstruującej (createItem) zwraca obiekt typu Item, więc to mówi nam od razu, że wszystkie trzy obiekty muszą implementować ten sam interfejs lub rozszerzać tę samą klasę.
class DocumentItemProducer implements ItemFactory {

    @Override
    public Item createItem() {
        DocumentItem documentItem = new DocumentItem();
        documentItem.fillTemplates();
        return documentItem;
    }
}
class AttachmentItemProducer implements ItemFactory {

    @Override
    public Item createItem() {
        AttachmentItem attachmentItem = new AttachmentItem();
        attachmentItem.fillTemplates();
        return attachmentItem;
    }
}
class NoteItemProducer implements ItemFactory {

    @Override
    public Item createItem() {
        NoteItem noteItem = new NoteItem();
        noteItem.fillTemplates();
        return noteItem;
    }
}
Poza tworzeniem obiektów w ramach metody createItem jest wywoływana jeszcze metoda fillTemplates, ale nie będziemy się nią teraz jakoś specjalnie zajmować. Na tym etapie wystarczy wiedzieć, że wypełnia ona każdy z obiektów dodatkowymi danymi.

Metoda fabrykująca

Dochodzimy do samego sedna, czyli do interfejsu fabryki ItemFactory. W nim znajduje się deklaracja metody createItem, która jest implementowana przez wszystkie klasy dziedziczące (przez wszystkich producentów). Mamy więc jedną metodę fabryki, która fabrykuje (wytwarza) na różne sposoby obiekty tego samego interfejsu Item. Mówimy, że wytwarzanie odbywa się na różne sposoby, ponieważ każda klasa dostarcza własną, niezależną implementację interfejsu Item (DocumentItem, AttachmentItem, NoteItem). Tak właśnie działa metoda fabrykująca!
interface ItemFactory {

    Item createItem();
}

Produkty

Cały czas mówimy o tym, że klasy producentów tworzą obiekty ("produkty") klas rozszerzających abstrakcję Item. Zobaczmy więc w końcu, jak wyglądają klasy owych produktów:
class DocumentItem extends Item {

    @Override
    void fillTemplates() {
        templates.put("title", "<>");
        templates.put("content", "<>");
    }
}
class AttachmentItem extends Item {

    @Override
    void fillTemplates() {
        templates.put("name", "");
        templates.put("extension", "");
        templates.put("fullName",
                      String.format("%s - %s.%s%s",
                                    "<>"));
    }
}
class NoteItem extends Item {

    @Override
    void fillTemplates() {
        templates.put("content", "<<Please write your notes here>>");
    }
}
W najprostszej wersji mogą to być puste klasy dziedziczące z pustej klasy abstrakcyjnej Item, jednak my chcemy, aby nasze klasy miały funkcjonalność. Chcemy wykonać część zasadniczą naszego zadania, a więc wykorzystać wzorzec Metoda fabrykująca do stworzenia wartościowych szablonów. Z tego powodu każda z metod przygotowuje nazwę pola dokumentu wraz z opisem informującym użytkownika, co powinno się znaleźć w tym miejscu (co powinien wpisać lub dodać, na przykład w polu content w ramach notatki powinien wpisać swoją...notatkę). Może tego tutaj nie widać, ale za to dosyć łatwo można się domyślić, że dane wrzucamy do mapy (templates), inicjowanej w klasie Item.

Tworzenie szablonów z użyciem wzorca

W poprzednim paragrafie przedstawiliśmy klasy implementujące abstrakcję Item. W metodzie fillTemplates wypełniamy tam mapę przechowującą nazwę pola dokumentu wraz z szablonem. Teraz pozostało nam zajrzeć do części abstrakcyjnej, z której korzystają wszystkie klasy (klasa Item).

Tworzymy tutaj mapę szablonów. Kluczem w mapie będzie nazwa pola w dokumencie (załączniku lub notatce), a wartością predefiniowany tekst oznajmiający użytkownikowi, co ma wpisać w danym polu. Zakładamy, że każda forma dokumentu będzie wymagała miejsca na podpis oraz na datę podpisania dokumentu, dlatego w konstruktorze uruchamianym przez każdą z klas dziedziczących wkładamy do mapy (za pomocą metody initTemplates) kolejno "whoCreated" i "whenCreated". Niezależnie od tego, czy stworzymy obiekt klasy DocumentItem, AttachmentItem, czy inny, każdy z obiektów będzie miał wypełnione w mapie te wartości.
import java.util.HashMap;
import java.util.Map;

abstract class Item {

    Map<String, String> templates = new HashMap<>();

    public Item() {
        initTemplates();
    }

    void initTemplates() {
        templates.put("whoCreated", "<<Your login will appear here>>");    
        templates.put("whenCreated", "<<Creation date here>>");
    }

    abstract void fillTemplates();

    Map<String, String> getTemplates() {
        return templates;
    }

    void putTemplateValue(String label, String value) {
        templates.put(label, value);
    }
}
Pozostałe szablony są już specyficzne dla danego rodzaju dokumentu, dlatego są one dodawane z poziomu dedykowanych klas. Wykorzystujemy do tego celu metodę fillTemplates, którą nadpisujemy w klasach dziedziczących, na przykład w klasie DocumentItem:
class DocumentItem extends Item {

    @Override
    void fillTemplates() {
        templates.put("title", "<>");
        templates.put("content", "<>");
    }
}
To tyle, jeśli chodzi o strukturę klas tworzonych przez fabrykę. Przypomnijmy sobie teraz, jak uruchamiamy metodę wypełniającą szablony. Dzieje się to zaraz po tym, jak fabryka stworzy obiekt dokumentu. W momencie wywołania konstruktora uruchomiony jest kod konstruktora nadklasy Item, gdzie metoda initTemplates wrzuca do mapy szablon do wprowadzenia autora oraz daty tworzenia dokumentu.

W kolejnej linii uruchamiamy metodę fillTemplates, która wypełnia mapę danymi specyficznymi dla dokumentu podstawowego. Analogicznie działa to dla załącznika i notatki.
class DocumentItemProducer implements ItemFactory {

    @Override
    public Item createItem() {
        DocumentItem documentItem = new DocumentItem();
        documentItem.fillTemplates();
        return documentItem;
    }
}

Uruchomienie

Podsumowując mamy fabrykę do produkcji różnych rodzajów dokumentów. Mamy też system przechowywania szablonów zbudowany na grupie klas implementujących interfejs Item. Teraz wystarczy to wszystko uruchomić. Co ważne, sama fabryka to...tylko fabryka i nie ma nic wspólnego z szablonami. Szablony dodaliśmy, aby pokazać przykład jej wykorzystania.
public class Start {

    public static void main(String[] args) {

        // Tworzymy obiekt producenta dokumentów, działającego pod kontrolą fabryki 
        DocumentItemProducer documentItemProducer = new DocumentItemProducer();
        Item document = documentItemProducer.createItem();

        // Tworzymy obiekt producenta załączników 
        AttachmentItemProducer attachmentItemProducer = new AttachmentItemProducer();
        Item attachment = attachmentItemProducer.createItem();

        // Tworzymy obiekt producenta notatek
        NoteItemProducer noteItemProducer = new NoteItemProducer();
        Item note = noteItemProducer.createItem();

        // Drukujemy szablony dla pól dokumentu
        documentTemplate(document);

        // Wypełniamy szablony przykładowymi danymi
        documentUseCase(document);
    }

    private static void documentTemplate(Item document) {
    
        System.out.println(String.format("%s: %s", "Document title",
                                                document.getTemplates().get("title")));
        System.out.println(String.format("%s: %s", "Who created", 
                                                    document.getTemplates().get("whoCreated")));
        System.out.println(String.format("%s: %s", "When document was created", 
                                                        document.getTemplates().get("whenCreated")));
        System.out.println("\n");
    }
    
    private static void documentUseCase(Item document) {

        document.putTemplateValue("title", "Kontrakt b2b");
        document.putTemplateValue("whoCreated", "Jan Kowalski");        
        document.putTemplateValue("whenCreated",
                              DateTimeFormatter.ofPattern("YYYY-MM-dd HH:mm").format(LocalDateTime.now()));

        System.out.println(String.format("%s: %s", "Document title", 
                                                        document.getTemplates().get("title")));
        System.out.println(String.format("%s: %s", "Who created", 
                                                            document.getTemplates().get("whoCreated")));
        System.out.println(String.format("%s: %s", "When document was created", 
                                                                document.getTemplates().get("whenCreated")));
    }
}
Wynik wykonania kodu:
Java 8 - Data i czas przed Javą 8

Gdzie używamy wzorca Metoda fabrykująca ?

Wzorca używamy wszędzie tam, gdzie przewidujemy potrzebę tworzenia wielu różnych obiektów jednego interfejsu. Takie rozwiązanie odseparuje nam główny kod biznesowy naszego programu od obszaru, w którym tylko produkujemy obiekty. W ten sposób w przyszłości łatwiej będzie dołożyć nowe klasy implementujące wspomniany interfejs, a z drugiej strony taka rozbudowa klas nie skomplikuje zasadniczej części kodu. Takie odseparowanie kodu nazywamy hermetyzacją.

Fabryka, poprzez to, że produkuje obiekty konkretnego interfejsu, skupia się na produkcji konkretnej grupy obiektów. Biorąc pod uwagę to, że praktycznie w każdej aplikacji mamy do czynienia z takimi grupami, dosyć łatwo można przytoczyć przypadki użycia. I tak na przykład budując system do rezerwacji biletów na wydarzenia, możemy zaimplementować fabrykę EventFactory, produkującą obiekty klasy Event. Wtedy będzie ona tworzyć szczegółowe implementacje pod postaciami MusicEvent, SportEvent, ScienficEvent czy MotivationalEvent. W takiej sytuacji będziemy mogli stworzyć abstrakcyjną klasę Event i wypełnić ją danymi takimi jak nazwa, czy data rozpoczęcia i zakończenia eventu (cecha wspólna wszystkich eventów). Natomiast klasy dziedziczące będą zawsze dokładać "coś od siebie". Na przykład w ramach wydarzenia muzycznego może to być rodzaj granej muzyki, a w ramach eventu naukowego definicja eksperymentu.
Zdjęcie autora
Autor: Jarek Klimas
Data: 03 stycznia 2024
Labele: Backend, Podstawowy, Java
Jeśli interesują Cię wzorce projektowe, możesz być również zainteresowany naszym Kursem Aplikacji Web, w którym uczysz się kompleksowo Springa, Hibernate'a i innych aktualnych rozwiązań technologicznych na bazie gotowej aplikacji webowej. Oto co dokładnie znajdziesz w kursie:
  • Kurs implementacji aplikacji webowej zbudowanej za pomocą:
    • Spring Boot 2, Spring Framework 5, Spring Data JPA, Spring MVC
    • Spring Security, Spring AOP
    • Hibernate
    • REST Api, Format JSON
    • Maven
    Aplikacja obejmuje funkcjonalności od rejestracji użytkownika i logowania, przez tworzenie, edycję oraz usuwanie danych, aż po różne formy prezentacji, w tym tabele i wykresy. Kurs obejmuje również implementację resetowania hasła z linkiem potwierdzającym wysyłanym na adres email.
  • Atak CSRF i jak się przed nim bronić w Springu
  • Clickjacking za pomocą iframe - jak się zabezpieczyć za pomocą Spring Security
  • CORS i jego możliwe opcje w Springu
  • Testowanie CORS-a z użyciem cURL
  • Dodatkowy projekt do nauki samego Hibernate'a, przygotowany w dwóch wersjach (z Lombokiem i bez). Ta część bazy wiedzy zawiera ponad 60 omówionych snippetów kodu, wzbogaconych dodatkowo ponad 40 zdjęciami. W projekcie zostały zaimplementowane i omówione między innymi:
    • Podstawy Hibernate'a (adnotacje, encje itp.)
    • Wszystkie rodzaje relacji bazodanowych
    • Orphan Removal
    • Single Table Discriminator
    • Table Per Class
  • ...oraz wiele innych zagadnień.
Sam kurs implementacji aplikacji to ponad 150 stron (całość online) analizy kodu od frontendu przez REST-owe wysyłanie żądań i odbieranie odpowiedzi, po zapis w bazie i wizualizowanie zapisu we frontendzie. Dokładnie rozpisane ścieżki w kodzie z tabelami kolejnych kroków dla poszczególnych funkcjonalności. Drogowskazy do ogromnej bazy wiedzy dołączonej w postaci kursów Spring i Hibernate (z projektami wspierającymi naukę).
Zobacz aplikację

Stale się rozwijamy, a więc bądź na bieżąco!
Na ten adres będziemy przesyłać informacje o ważniejszych aktualizacjach, a także o nowych materiałach pojawiających się na stronie.
Polub nas na Facebooku:
Nasi partnerzy: stackshare
Javappa to również profesjonalne usługi programistyczne oparte o technologie JAVA. Jeśli chesz nawiązać z nami kontakt w celu uzyskania doradztwa bądź stworzenia aplikacji webowej powinieneś poznać nasze doświadczenia.
Kliknij O nas .


Pozycjonowanie stron: Grupa TENSE