Wzorce projektowe w programowaniu – wzorzec Fabryka

Wzorzec Fabryka – wprowadzenie

Niniejszy wpis dotyczy kolejnego wzorca projektowego – Fabryki (ang. Factory). Zgodnie z regułami projektowania nie powinniśmy tworzyć implementacji, lecz interfejsy. Ale co w przypadku, gdy należy stworzyć nowy obiekt i użyć operatora „new”? Przecież kłóci się to z dobrymi praktykami tworzenia kodu, mówiącymi o elastyczności.

Kiedy korzystamy z operatora „new” tworzymy nowy obiekt, który jest reprezentacją rzeczywistej klasy, tworzymy implementację a nie interfejs. Jednak „dowiązywanie” kodu programu do definicji konkretnej klasy powoduje, że cały kod staje się mniej elastyczny a aktualizując program o wiele łatwiej o przypadkową, negatywną zmianę jego funkcjonalności.

Posilek posilek = new KotletZiemniaki();

Posiadając już mnogość powiązanych ze sobą rzeczywistych klas, często jesteśmy zmuszeni do tworzenia kodu jak poniżej.

Posilek posilek;

if (sniadanie) {
     posilek = new PlatkiSniadaniowe();
}
else if (obiad) {
     posilek = new KotletZiemniaki();
}
else if (deser) {
     posilek = new Lody();
}
else if (kolacja) {
     posilek = new Kanapka();
}

Widzimy, że możemy mieć całe mnóstwo klas opisujących posiłek, jednak nie wiemy, której klasy należy użyć do utworzenia nowego obiektu. Wiedzieć to będziemy dopiero podczas działania programu i spełnienia określonych warunków. Jeżeli będziemy musieli zmienić wymagania utworzenia nowego obiektu lub wprowadzić aktualizacje, to zmuszeni jesteśmy do ingerowania w kod już istniejący, co powodować może, jak wspomniano wyżej, duże ryzyko pomyłek i negatywnej ingerencji w działający już program.

Czy można jakoś tego uniknąć? Otóż, programując pod kątem użycia interfejsów możemy odizolować się od wielu zmian, które z czasem mogą być nieuniknione. Dzięki mechanizmowi polimorfizmu program będzie współpracował z dowolną klasą posiadającą taki interfejs. Jeżeli natomiast, nasz kod wykorzystuje większą liczbę klas rzeczywistych, to w sytuacji, kiedy trzeba będzie dodać nowe klasy rzeczywiste, trzeba będzie solidnie kod zmodyfikować. A więc, nie będzie on zamknięty na zmiany (jak głoszą praktyki optymalnego projektowania).

Załóżmy, że mamy prosperujące biuro podróży. Napiszmy kod luźno powiązany z tym faktem.

Podroz wykupPodroz() {
     Podroz podroz = new Podroz();
     podroz.zamowTransport();
     podroz.zarezerwujNoclegi();
     podroz.zarezerwujAtrakcje();
     return podroz;
}

Aby podróż doszła do skutku należy: zamówić odpowiedni środek transportu, zarezerwować noclegi w wybranej przez nas miejscowości oraz (w razie potrzeby) kupić bilety na interesujące nas atrakcje na miejscu.

Ale oczywiście potrzebnych nam jest wiele rodzajów podróży (kilka miejsc, możliwość transportu autokarem lub samemu itd.). Więc dodajmy dodatkowe wiersze kodu.

Podroz wykupPodroz(String type) {
     Podroz podroz;
     if (type.equals("egipt")) {
          podroz = new PodrozEgipt();
     }
     else if (type.equals("hawaje")) {
          podroz = new PodrozHawaje();
     }
     else if (type.equals("grenlandia") {
          podroz = new PodrozGrenlandia();
     }
     podroz.zamowTransport();
     podroz.zarezerwujNoclegi();
     podroz.zarezerwujAtrakcje();
     return podroz;
}

Teraz jako argument do metody wykupPodroz przekazujemy rodzaj podróży. Opierając się o informację dotyczącą rodzaju wybranej podróży tworzymy obiekt danej klasy i przypisujemy go do zmiennej obiektowej podroz. Pamiętajmy, że każda z klas (obrazujących rodzaje podróży) musi mieć zaimplementowany interfejs Podroz. Następnie zamawiamy odpowiedni transport itp. (nie wchodzimy w szczegóły, jak poszczególne metody działają).

Jednak przyszedł czas na aktualizację oferty i należałoby dodać dodatkowe miejsca podróży. Ponadto podróż na Grenlandię jest już niemożliwa z powodu panujących tam warunków, więc należy ją z oferty usunąć.

Podroz wykupPodroz(String type) {
     Podroz podroz;
     if (type.equals("egipt")) {
          podroz = new PodrozEgipt();
     }
     else if (type.equals("hawaje")) {
          podroz = new PodrozHawaje();
     }
     // usuwamy, może kiedyś do tego wrócimy
     /*
     else if (type.equals("grenlandia") {
          podroz = new PodrozGrenlandia();
     }
     */
     else if (type.equals("norwegia") {
          podroz = new PodrozNorwegia();
     }
     else if (type.equals("Tokyo") {
          podroz = new PodrozTokyo();
     }
     podroz.zamowTransport();
     podroz.zarezerwujNoclegi();
     podroz.zarezerwujAtrakcje();
     return podroz;
}

Widzimy, że to nie jest kod zamknięty na zmiany. Za każdym razem, gdy będziemy chcieli dodać lub usunąć coś z oferty, trzeba ingerować w powyższy fragment kodu (który działa mógł zostać napisany na samym początku produkcji aplikacji).

Jednak możemy założyć i wyodrębnić część kodu, która pozostanie taka sama.

podroz.zamowTransport();
podroz.zarezerwujNoclegi();
podroz.zarezerwujAtrakcje();
return podroz;

Zobaczmy, że to własnie kwestia związana z wyborem klasy, która powinna zostać użyta przy tworzeniu obiektu powoduje zamieszanie w metodzie wykupPodroz() i blokuje jej zamknięcie na modyfikacje. Niemniej, wiemy już, która część kodu podlega zmianom a jaka nie, więc możemy skorzystać z hermetyzacji.

Hermetyzacja tworzenia obiektów

Zabierzmy cały kod odpowiadający za tworzenie obiektów i przełóżmy go do innego obiektu, który będzie odpowiadał tylko za rezerwację i organizację podróży. Obiekt będzie implementacją klasy „ProstaFabrykaPodrozy” – jego zadaniem jest tworzenie obiektów odpowiadających poszczególnym rodzajom podróży.

public class ProstaFabrykaPodrozy {
     public Podroz utworzPodroz(String type) {
          Podroz podroz = null;
          if (type.equals("egipt")) {
               podroz = new PodrozEgipt();
          }
          else if (type.equals("hawaje")) {
               podroz = new PodrozHawaje();
          }
          else if (type.equals("norwegia") {
               podroz = new PodrozNorwegia();
          }
          else if (type.equals("Tokyo") {
               podroz = new PodrozTokyo();
          }

Ten kod oczywiście nadal jest uzależniony od poszczególnych rodzajów podróży, tak jak poprzednio (jednak zaraz się tym zajmiemy).

Jakie są zalety takiego postępowania? Ponieważ teraz, z powstałej fabryki, może korzystać wiele „klientów” – innych klas. Na przykład klasa PodrozeMenu (wykorzystuje fabrykę do pobierania informacji o poszczególnych rodzajach podróży – tworząc poszczególne obiekty tychże rodzajów z niezbędnymi opisami w ich wnętrzach) itd.

Stwórzmy zatem klasę BiuroPodrozy i poprawmy nasz kod.

public class BiuroPodruzy {
     FabrykaPodrozy fabryka;
     public BiuroPodrozy(ProstaFabrykaPodrozy fabryka) {
          this.fabryka = fabryka;
     }
     public Podroz wykupPodroz(String type) {
          Podroz podroz;
          podroz = fabryka.utworzPodroz(type);
          podroz.zamowTransport();
          podroz.zarezerwujNoclegi();
          podroz.zarezerwujAtrakcje();
          return podroz;
     }

Zwróćmy uwagę, że w metodzie wykupPodroz() zamieniono operator new na metodę utworzPodroz(), która zdefiniowana jest w klasie FabrykaPodrozy (obiekt fabryka). Dzięki temu pozbywamy się w tym miejscu tworzenia nowych obiektów.

To co przed chwilą zrobiliśmy nosi nazwę Simple Factory i nie jest pełnoprawnym wzorcem projektowym. Jest to bardziej coś, co wytworzyło się jako osobny twór wzorca Fabryka i zyskało na tyle dużą popularność, że przez niektórych uznawany jest za samodzielny. Niemniej, używa się go bardzo często. Spójrzmy na poniższy diagram.

Obiekt klasy BiuroPodrozy to klient naszej fabryki. Aby nasze biuro mogło :otrzymywać” kolejne podróże, musi korzystać z pośrednictwa ProstejFabrykiPodrozy.
ProstaFabrykaPodrozy – to właśnie fabryka, w której „produkowane” są poszczególne rodzaje podróży (to powinna być jedyna część naszej aplikacji, która odwołuje się do rzeczywistych klas opisujących poszczególne rodzaje podróży).
Podroz – obiekt tej klasy jest gotowym „produktem” naszej fabryki. Klasa Podroz została zdefiniowana jako klasa abstrakcyjna, posiadająca kilka zaimplementowanych zachowań , które mogą być w bardzo łatwy sposób przesłonięte.
Obiekty klas PodrozHawaje/Egipt/Norwegia/Tokyo – to są nasze produkty rzeczywiste. Każdy produkt musi posiadać zaimplementowany interfejs Podroz (który w tym przypadku oznacza rozszerzenie funkcjonalności abstrakcyjnej klasy Podroz. Pamiętajmy, że gdy mówimy o wzorcach, implementacja interfejsu nie zawsze oznacza to, że należy utworzyć klasę, która w swojej deklaracji słowo „implements” – jeżeli klasa rzeczywista implementuje daną metodę ze swojego supertypu (którym może być klasa lub interfejs), to nadal traktujemy ją jako mają zaimplementowany interfejs swojego supertypu. Każdy produkt musi również być konkretną implementacją (obiektem danej klasy).

Nowa struktura

SimpleFactory jest bardzo przydatne, niemniej może się okazać, że jego idea nie pasuje do naszej aplikacji. Np. może dojść do przypadku, gdy Prosta Fabryka będzie musiała wytwarzać różny rodzaj tych samych produktów w różnych miejscach kodu. Lub też, gdy zajdzie potrzeba dodania czegoś nowego do aplikacji, programista na nowo będzie musiał tworzyć Prostą Fabrykę, aby właśnie dostosować ją do wymagań, jakie mają spełniać nowo tworzone obiekty w nowej części aplikacji. Wtedy należałoby umieścić wszystkie operacje związane z produkcją nowych obiektów w klasie będącej klientem (u nas BiuroPodrozy) i jednocześnie zapewnić możliwość tworzenia „regionalnych” (odpowiadających określonym wymaganiom) obiektów w nowo powstałej części kodu.

Zatem wprowadźmy pewne zmiany co do wcześniejszych wersji naszego kodu. umieśćmy metodę utworzPodroz() z powrotem klasy BiuroPodrozy, ale tym razem zadeklarujmy ją jako klasę abstrakcyjną i będziemy tworzyć klasy podrzędne do niej.

public abstract class BiuroPodrozy {
     public Podroz wykupPodroz(String type) {
          Podroz podroz;
          podroz = utworzPodroz(type);
          podroz.zamowTransport();
          podroz.zarezerwujNoclegi();
          podroz.zarezerwujAtrakcje();
          return podroz;
     }
     
     abstract Podroz utworzPodroz(String type);
}

Obiekt reprezentujący fabrykę został przesunięty do metody utworzPodroz() – jest to tzw. metoda fabryczna.

Teraz w przypadku dodania nowych możliwości do aplikacji i konieczności skorzystania z inaczej funkcjonującej Fabryki, należy utworzyć po prostu osobną podklasę (czyli naszą Fabrykę) do klasy głównej (BiuroPodrozy). Inaczej funkcjonującej Fabryki – w naszym przypadku będą to: PremiumBiuroPodrozy (zamawia transport tylko z klimatyzacją i atrakcje do kwoty 300 zł za szt.) oraz EkonomiczneBiuroPodrozy (zamawia transport także bez klimatyzacji a atrakcje ogranicza do kwoty 100 zł za szt.).

W każdej klasie podrzędnej pojawia się metoda utworzPodroz(), która przesłania swój pierwowzór z klasy nadrzędnej i tworzy obiekty danej klasy rzeczywistej, w tym przypadku danej podklasy (utworzPodroz() jest metodą abstrakcyjną, więc wszystkie podklasy muszą mieć ją zaimplementowaną). Jednocześnie wszystkie podklasy wykorzystują metodę wykupPodroz() zdefiniowaną w klasie nadrzędnej BiuroPodrozy.

W przypadku konieczności „utworzenia” podróży dla biznesmenów wykorzystujemy podklasę PremiumBiuroPodrozy, aby zapewnić najlepsze warunki i najdroższe atrakcje.

public class PremiumBiuroPodrozy extends BiuroPodrozy {
     
     public Podroz utworzPodroz(String type) {
          if (type.equals("egipt")) {
               podroz = new PremiumPodrozEgipt();
          }
          else if (type.equals("hawaje")) {
               podroz = new PremiumPodrozHawaje();
          }
          else if (type.equals("norwegia") {
               podroz = new PremiumPodrozNorwegia();
          }
          else if (type.equals("Tokyo") {
               podroz = new PremiumPodrozTokyo();
          }
     }
}

Finalnie, nasza klasa nadrzędna wygląda tak.

public abstract class BiuroPodrozy {
     public Podroz wykupPodroz(String type) {
          Podroz podroz;
          podroz = utworzPodroz(type);
          podroz.zamowTransport();
          podroz.zarezerwujNoclegi();
          podroz.zarezerwujAtrakcje();
          return podroz;
     }
     protected abstract Podroz utworzPodroz(String type);
}

Cała odpowiedzialność za tworzenie obiektów sprowadzona została do oddzielnej metody typu Fabryka:
– jest ona metodą abstrakcyjną, dzięki czemu za obsługę tworzenia obiektów odpowiedzialne są klasy podrzędne,
– zwraca ona produkt, który zazwyczaj jest wykorzystywany przez metody definiowane w superklasie,
– powoduje, że kod klienta (czyli superklasy, w której metoda fabrykująca się znajduje) nie wie, jaki rodzaj produktu będzie aktualnie tworzony,
– może być sparametryzowana, co pozwala na wybór wielu produktów.

Tworzenie obiektów przy pomocy metody typu Fabryka

Zobaczmy, jak teraz przebiega proces tworzenia (organizowania) naszej podróży. Załóżmy, że organizujemy podróż dla wpływowego akcjonariusza firmy na Hawaje.

Najpierw tworzymy nową instancję obiektu klasy PremiuBiuroPodrozy:

BiuroPodrozy premiumBiuroPodrozy = new PremiumBiuroPodrozy();

Następnie, wywołujemy metodę wykupPodroz(), która jest metodą instancji obiektu premiumBiuroPodrozy (metoda ta jest dziedziczona z klasy bazowej BiuroPodrozy):

premiumBiuroPodrozy.wykupPodroz("Hawaje");

Dalej, powyższa metoda wykupPodroz() wywołuje w sobie metodę utworzPodroz():

Podroz podroz = utworzPodroz("Hawaje");

Kompilacja

Wiemy już, na jakiej zasadzie działa tworzenie obiektów. Teraz pora na rzeczywistą implementację. Stwórzmy więc niezbędny kod, który nam to umożliwi.

Najpierw utwórzmy abstrakcyjną klasę Podroz, po której będą dziedziczyły wszystkie klasy rzeczywiste reprezentujące poszczególne rodzaje podróży. Klasa ta zapewnia zestaw podstawowych, domyślnych procedur opisujących proces organizacji wycieczki: zamawianie transportu, noclegów oraz atrakcji.

public abstract class Travel {

    String name;
    String transport;
    String attractions;

    void organization() {
        System.out.println("Organizowanie wycieczki: " + name);
    }

    public void transportOrder() {
        System.out.println("Szukanie wolnych przewoźników / samochodów na wynajem.");
    }

    public void accomodation() {
        System.out.println("Rezerwowanie noclegów.");
    }

    public void bookingOfAttractions() {
        System.out.println("Rezerwacja interesującyh atrakcji.");
    }

    public String getName() {
        return name;
    }
}

Teraz zajmijmy się kilkoma klasami podrzędnymi. Idąc za przykładem, niech będą to podróże: ekonomiczna i premium do Egiptu.

public class EcomonicTravelEgypt extends Travel {

    public EcomonicTravelEgypt () {
        name = "Ekonomiczna wycieczka na Twoją kieszeń.";
        transport = "Autokar bez klimatyzacji.";
        attractions = "Podstawowe atrakcje Egiptu.";
    }
}

public class PremiumTravelEgypt extends Travel {

    public PremiumTravelEgypt () {
        name = "Wycieczka dla wymagających po ekskluzywnych miejscach w Egipcie.";
        transport = "Autokar wyposażony w klimatyzację, osobiste ekrany oraz słuchawki.";
        attractions = "Nietanie, ale zapierające dech w piersiach atrakce";
    }
}

Na koniec, przygotujmy środowisko testowe. Tworzymy dwie różne Fabryki – premium i economic, a następnie używamy ich do stworzenia wymaganych obiektów (wycieczka premium oraz ekonomiczna).

public class TravelTest {

    public static void main(String[] args) {

        TravelAgency premium = new PremiumTravelAgency();
        TravelAgency economic = new EconomicTravelAgency();

        Travel travel = premium.buyTravel("Egipt");
        System.out.println("Wykupiliśmy wycieczkę: " + travel.getName() + "\n");

        travel = economic.buyTravel("Egipt");
        System.out.println("A teraz wykupiliśmy wycieczkę: " + travel.getName() + "\n");
    }
}

Wyniki przedstawiono poniżej.

Wzorzec Metoda Fabrykująca

Istnieje kilka wzorców typu Fabryka. Każdy z nich wykorzystuje hermetyzację procesu tworzenia obiektów. Wzorzec Metoda Fabrykująca hermetyzuje ten proces poprzez pozwolenie klasom podrzędnym na decydowanie, jaki obiekt zostanie utworzony. Przeanalizujmy diagram, który pojawił się już wcześniej.

Klasy Fabryki

BiuroPodrozy to nasza klasa abstrakcyjna reprezentująca fabrykę. Definiuje ona abstrakcyjną metodę fabrykującą, którą implementują poszczególne klasy podrzędne w celu tworzenia odpowiednich produktów. Bardzo często fabryka wykorzystuje kod, który jest uzależniony od produktów abstrakcyjnych tworzonych przez klasy podrzędne. Fabryka nigdy nie wie, jaki produkt rzeczywisty został utworzony.
Klasy, które tworzą określone produkty nazywane są fabrykami rzeczywistymi (PremiumBiuroPodrozy oraz EkonomiczneBiuroPodrozy). Znajdująca się w nich metoda utworzPodroz() jest metodą fabrykującą, która tworzy poszczególne produkty.

Mamy więc klasy produktów.

Fabryki wytwarzają produkty – u nas produktem biura podróży są podróże. Rożne rodzaje podróży premium i ekonomicznych są produktami rzeczywistymi – one wszystkie są produkowane przez poszczególne biura.

Wzorzec Metoda Fabrykująca zapewnia stworzenie odpowiedniej struktury klas poprzez ustanowienie metody wykupPodroz() połączonej z metodą fabrykującą.

Definicja tego wzorca jest następująca.

Wzorzec Metoda Fabrykująca definiuje interfejs pozwalający na tworzenie obiektów, ale pozwala klasom podrzędnym decydować, jakiej klasy obiekt zostanie utworzony. Wzorzec Metoda Fabrykująca przekazuje odpowiedzialność za tworzenie obiektów do klas podrzędnych.

Tak jak w przypadku każdej innej fabryki, wzorzec Metoda Fabrykująca daje możliwość hermetyzacji tworzenia obiektów typów rzeczywistych. Patrząc na poniższy diagram widzimy, że abstrakcyjna klasa fabrykująca daje nam interfejs wyposażony w metodę umożliwiającą tworzenie obiektów, znaną również pod nazwą „metody fabrykującej”. Wszystkie inne metody zaimplementowane w abstrakcyjnej klasie fabryki operują na produktach tworzonych przez metodę fabrykującą.

Klasa Fabryka posiada implementacje wszystkich metod służących do operowania na produktach, za wyjątkiem metody wytwarzającej te produkty (metodaFabrykująca()) – metodę tą implementują fabryki rzeczywiste.
Wszystkie produkty muszą posiadać ten sam interfejs, aby klasy wykorzystujące te produkty, mogły się do nich odwoływać za pośrednictwem konkretnego interfejsu (a nie bezpośredniej klasy rzeczywistej).

Zależności pomiędzy obiektami

Kiedy bezpośrednio tworzymy obiekt danej klasy, w pewien sposób uzależniamy się od definicji tej klasy rzeczywistej. Spójrzmy na kod, który obrazuje, co by się stało, gdybyśmy nie skorzystali z Fabryki.

public class BiuroPodrozyUzaleznione {
     
     public Podroz utworzPodroz (String style, String type) {
          Podroz podroz = null;
          if (style.equals("Ekonomiczne")) {
               if (type.equals("Egipt")) {
                    podroz = new EkonomicznePodrozEgipt;
               } else if (type.equals("Hawaje")) {
                    podroz = new EkonomicznePodrozHawaje;
               } else if (type.equals("Grenlandia")) {
                    podroz = new EkonomicznePodrozGrenlandia;
               } else if (type.equals("Norwegia")) {
                    podroz = new EkonomicznePodrozNorwegia;
               } else if (type.equals("Tokyo")) {
                    podroz = new EkonomicznePodrozTokyo;
               }
          } else if (style.equals("Premium")) {
               if (type.equals("Egipt")) {
                    podroz = new PremiumPodrozEgipt;
               } else if (type.equals("Hawaje")) {
                    podroz = new PremiumPPodrozHawaje;
               } else if (type.equals("Grenlandia")) {
                    podroz = new PremiumPPodrozGrenlandia;
               } else if (type.equals("Norwegia")) {
                    podroz = new PremiumPPodrozNorwegia;
               } else if (type.equals("Tokyo")) {
                    podroz = new PremiumPodrozTokyo;
               }
          } else {
               System.out.pritnln("Niepoprawny rodzaj.");
               return null;
          }
          podroz.zamowTransport();
          podroz.zarezerwujNoclegi();
          podroz.zarezerwujAtrakcje();
          return podroz;
     }
}

Widzimy, że redukcja zależności kod od implementacji klas rzeczywistych jest rzeczą bardzo dobrą. Z tego względu istnieje osobna reguła formalizująca ten zamiar – reguła Odwrócenia Zależności (ang. Dependency Inversion).

Uzależniaj kod od abstrakcji, a nie od klas rzeczywistych.

Chodzi w niej o to, że składniki wysokiego poziomu – klasy, których zachowania zostały zdefiniowane przy użyciu zachowań innych składników niskiego poziomu (BiuroPodrozy jest u nas składnikiem wysokiego poziomu, ponieważ jego zachowania są powodowane przez poszczególne rodzaje podróży). Zamiast tego, składniki na obu poziomach powinny zależeń tylko od elementów abstrakcyjnych.

Postępowanie zgodnie z powyższą regułą jest możliwe dzięki poniższym wskazówkom:
– żadna zmienna nie powinna przechowywać odwołania do klasy rzeczywistej (jeżeli korzystamy z operatora new otrzymujemy w zamian odwołanie do klasy rzeczywistej; unikamy tego korzystając z fabryki),
– żadna klasa nie powinna dziedziczyć z klasy rzeczywistej (ponieważ dziedzicząc po klasie rzeczywistej jesteśmy od niej uzależnieni; należy dziedziczyć po elementach abstrakcyjnych takich jak interfejsy czy l;asy abstrakcyjne),
– żadna metoda nie powinna przesłaniać metody zaimplementowanej w dowolnej z klas bazowych (jeżeli metoda klasy bazowej jest przesłonięta, oznacza to, że nie obecna klasa nie jest właściwą klasą bazową; metody klasy bazowej powinny być dopasowane do każdej z klas podrzędnych).

Zbudujmy jeszcze jedną fabrykę

Jak zostało wspomniane wcześniej, każde z biur podróży organizuje atrakcje w zależności od rodzaju klientów (premium, ekonomiczni). Można założyć, że ta sama atrakcja, będzie inaczej wyglądała w tych dwóch biurach – rejs tym samym statkiem w biurze premium będzie zawierać poczęstunek, szampana i miejsce na najwyższym pokładzie, natomiast klienci ekonomiczni spędzą ten czas bez poczęstunku na niższym pokładzie.

Zdefiniujmy interfejs dla fabryki zajmującej się wytwarzaniem atrakcji wycieczek.

public interface FabrykaAtrakcjiPodrozy {
     
     public Rejs utworzRjes();
     public Balon utworzLotBalonem();
     public Koncert utworzKoncert();
}

Teraz zajmijmy się implementacją ekonomicznej fabryki atrakcji podróży.

public class EkonomicznaFabrykaAtrakcjiPodrozy implements FabrykaAtrakcjiPodrozy {

     public Rejs utworzRejs() {
          return new rejsBezPoczestunku();
     }

     public Balon() {
          return new lotBalonemBezSzampana()
     }

     public Koncert() {
          return new koncertBezAutografu()
     }

Teraz należy zmodyfikować klasę Podroz, aby korzystała wyłącznie z atrakcji produkowanych w fabryce.

public abstract class Podroz {

    String nazwa;
    Rejs rejs;
    Balon balon;
    Koncert koncert;

    abstract void przygotowanie();

     void organizacja() {
        System.out.println("Organizowanie wycieczki: " + name);
    }

    public void zamawianieTransportu() {
        System.out.println("Szukanie wolnych przewoźników / samochodów na wynajem.");
    }

    public void zakwaterowanie() {
        System.out.println("Rezerwowanie noclegów.");
    }

    public void rezerwacjaAtrakcji() {
        System.out.println("Rezerwacja interesującyh atrakcji.");
    }

    public String pobierzNazwe() {
        return nazwa;
    }
}

W utworzonej abstrakcyjnej metodzie przygotowanie() gromadzimy wszystkie niezbędne atrakcje.

Teraz tworzymy klasę rzeczywistą danej podróży (np. do Egiptu). Zauważmy, że podróż do Egiptu premium oraz ekonomiczna różnią się między sobą tylko jakością atrakcji, ewentualnie zakwaterowaniem czy transportem. Główny filar pozostaje taki sam – wyjazd. A więc procedura przygotowania każdej z tych dwóch podróży jest identyczna – różnią się one tylko zawartością. Wynika stąd, że nie trzeba tworzyć osobnych klas (Premium i Ekonomiczna) do każdego rodzaju podróży – sprawą różnych atrakcji itp. zajmie się fabryka. Przygotujmy podróż do Egiptu (która ma takie atrakcje jak: lot balonem oraz koncert wieczorem).

public class EgiptPodroz extends Podroz {
     FabrykaAtrakcjiPodrozy fabrykaAtrakcjiPodrozy;

     public EgiptPodroz (FabrykaAtrakcjiPodrozy fabrykaAtrakcjiPodrozy) {
           this.fabrykaAtrakcjiPodrozy;
     }

     void przygotowanie () {
          System.out.println("Przygotowanie: " + nazwa);
          balon = fabrykaAtrakcjiPodrozy.utworzBalon();
          koncert = fabrykaAtrakcjiPodrozy.utworzKoncert();

Zaimplementujmy nasze rozwiązania w poszczególnych rodzajach podróży.

public class EkonomiczneBiuroPodrozy extends BiuroPodrozy {
     
     protected Podroz utworzPodroz (String item) {
          Podroz podroz = null;
          FabrykaAtrakcjiPodrozy fabrykaAtrakcji = new EkonomicznaFabrykaAtrakcjiPodrozy();
          
          if (item.equals("Egipt")) {
                podroz = new EgiptPodroz(fabrykaAtrakcji);
                podroz.ustawNazwa("Ekonomiczna wycieczka do Egiptu");
          } else if (item.equals("Hawaje")) {
                podroz = new HawajePodroz(fabrykaAtrakcji);
                podroz.ustawNazwa("Ekonomiczna wycieczka na Hawaje");
          } else if (item.equals("Norwegia")) {
                podroz = new NorwegiaPodroz(fabrykaAtrakcji);
                podroz.ustawNazwa("Ekonomiczna wycieczka ndo Norwegi);
          } else if (item.equals("Grenlandia")) {
                podroz = new GrenlandiaPodroz(fabrykaAtrakcji);
                podroz.ustawNazwa("Ekonomiczna wycieczka na Grenlandię");
          } else if (item.equals("Tokyo")) {
                podroz = new TokyoPodroz(fabrykaAtrakcji);
                podroz.ustawNazwa("Ekonomiczna wycieczka do Tokyo");
          }
          return podroz;
     }
}

Co udało nam się dokonać? Umożliwiliśmy tworzenie rodziny atrakcji dla poszczególnych rodzajów podróży – dzięki wprowadzeniu nowego typu fabryki, Fabryki Abstrakcyjnej.

Daje nam ona interfejs umożliwiający tworzenie całych rodzin produktów (w naszym przypadku są to atrakcje). Poprzez kod wykorzystujący interfejs odseparowaliśmy kod naszego programu (klienta) tworzącej odpowiednie produkty fabryki. Takie rozwiązanie pozwala na implementację wielu różnych fabryk tworzących produkty przeznaczone dla różnych podmiotów.

Teraz tworzenie podróży Ekonomicznej do Egiptu wygląda następująco. Tworzymy obiekt klasy EkonomiczneBiuroPodrozy.

BiuroPodrozy ekonomicznemBiuroPodrozy = new EkonomiczneBiuroPodrozy();

Wykupujemy wycieczkę.

ekonomiczneBiuroPodrozy.wykupPodroz("Egipt");

Następnie metoda wykupPodroz() wywołuje metodę utworzPodroz().

Podroz podroz = utworzPodroz("Egipt");

Wywołanie metody utworzPodroz() jest momentem, w którym włącza się fabryka.

Podroz podroz = new EgiptPodroz(ekonomicznaFabrykaAtrakcji);

Kolejnym etapem jest przygotowanie podróży. Po wywołaniu metody przygotowanie() fabryka jest proszona o tworzenie i dostarczanie kolejnych atrakcji. Używana jest fabryka ekonomiczna, stąd przygotowywana wycieczka balonem jest bez szampana a koncert bez autografów.

void przygotowanie() {
     balon = fabrykaAtrakcji.utworzBalon();
     koncert = fabrykaAtrakcji.utworzKoncert();
}

Na zakończenie otrzymujemy wycieczkę z atrakcjami. Resztę zadań przejmuje metoda wykupPodroz(), która zajmuje się organizowaniem transportu i noclegów.

Wzorzec Fabryka Abstrakcyjna dostarcza interfejs do tworzenia całych rodzin spokrewnionych lub zależnych od siebie obiektów bez konieczności określania ich klas rzeczywistych.

Zobrazujmy sobie na diagramie, jak to wszystko funkcjonuje.

A poniżej diagram przedstawiający nasze biuro podróży.

Fabryka Abstrakcyjna a Metoda Fabrykująca

Zauważmy, że każda metoda w fabryce abstrakcyjnej wygląda jak metoda fabrykująca (utworzBalon(), utworzKoncert() itp.). Wszystkie metody zostały zadeklarowane jako abstrakcyjne, a poszczególne klasy podrzędne dokonują ich przesłaniania tak, aby mogły tworzyć pewne obiekty. Czyli, metody fabryki abstrakcyjnej zostały zaimplementowane jako metody fabrykujące. Ma to sens, ponieważ zadaniem fabryki abstrakcyjnej jest zdefiniowanie interfejsu pozwalającego na tworzenie określonego zestawu produktów. Poszczególne metody tego interfejsu są odpowiedzialne za tworzenie produktów rzeczywistych. Aby wesprzeć ten proces, tworzymy kolejne podklasy, które dziedziczą po fabryce abstrakcyjnej.

Ideą Metody Fabrykującej jest zezwolenie superklasie na przekazywanie odpowiedzialności za tworzenie obiektów do klas podrzędnych. Wykorzystuje ona mechanizm dziedziczenia: tworzenie obiektów jest delegowane do klas podrzędnych implementujących metodę fabrykującą.

Natomiast ideą Fabryki Abstrakcyjnej jest tworzenie rodzin spokrewnionych obiektów bez konieczności polegania na ich klasach rzeczywistych. Wykorzystuje kompozycję obiektów: tworzenie obiektów zostało zaimplementowane w metodach dostępnych poprzez interfejs fabryki.

Niemniej wszystkie wzorce typu Fabryka promują tworzenie luźnych powiązań poprzez redukcję zależności kodu aplikacji od implementacji klas rzeczywistych.