Fabryka abstrakcyjna
Cel
Fabryka abstrakcyjna jest kreacyjnym wzorcem projektowym, który pozwala tworzyć rodziny spokrewnionych ze sobą obiektów bez określania ich konkretnych klas.
Problem
Wyobraź sobie, że tworzysz symulator sklepu meblowego. Twój kod składa się z klas, które reprezentują:
-
Rodzinę spokrewnionych produktów, powiedzmy:
Fotel
+Sofa
+StolikKawowy
. -
Różne warianty w ramach powyższej rodziny. Na przykład, produkty
Fotel
+Sofa
są dostępne w wariantach:Nowoczesny
,Wiktoriański
,ArtDeco
.
Trzeba produkować poszczególne meble w taki sposób, aby do siebie pasowały. Klienci nie cierpią bowiem otrzymywać mebli w zupełnie różnych stylizacjach.
Ponadto, nie chciałbyś zmieniać istniejącego kodu tylko po to, aby dodać nowy produkt lub rodzinę produktów do programu. Producenci mebli dość często wypuszczają nowe katalogi i nie chciałbyś zmieniać głównej części kodu za każdym razem gdy tak się stanie.
Rozwiązanie
Pierwszą rzeczą, jaką proponuje wzorzec projektowy Fabryka abstrakcyjna, jest wyraźne określenie interfejsów dla każdego konkretnego produktu z jakiejś rodziny (np. fotel, sofa, stolik kawowy). Następnie trzeba sprawić, aby wszystkie warianty produktów były zgodne z tymi interfejsami. Na przykład wszystkie fotele implementują interfejs Fotel
, wszystkie stoliki kawowe implementują interfejs StolikKawowy
i tak dalej.
Kolejnym krokiem jest deklaracja interfejsu Fabryka abstrakcyjna, który zawrze listę metod kreacyjnych wszystkich produktów w ramach jednej rodziny (na przykład, stwórzFotel
, stwórzSofę
, stwórzStolikKawowy
). Metody te muszą zwracać wyłącznie abstrakcyjne typy produktów, reprezentowane uprzednio określonymi interfejsami: Fotel
, Sofa
, StolikKawowy
i tak dalej.
A co z poszczególnymi wariantami produktów? Otóż, dla każdego wariantu rodziny produktów tworzymy osobną klasę fabryczną na podstawie interfejsu FabrykaAbstrakcyjna
. Klasa fabryczna to taka klasa, która zwraca produkty danego rodzaju. A więc, FabrykaNowoczesnychMebli
może zwracać wyłącznie obiekty: NowoczesneFotele
, NowoczesneSofy
oraz NowoczesneStolikiKawowe
.
Kod kliencki będzie korzystał z fabryk oraz produktów za pośrednictwem ich interfejsów abstrakcyjnych. Dzięki temu będzie można zmienić typ fabryki przekazywanej kodowi klienckiemu oraz zmienić wariant produktu jaki otrzyma kod kliencki i to wszystko bez ryzyka popsucia samego kodu klienckiego.
Załóżmy, że klient potrzebuje fabrykę do stworzenia fotela. Nie powinien musieć być świadom klasy tej fabryki, ani martwić się o rodzaj fotelu z jakim przyjdzie mu działać. Czy będzie to fotel nowoczesny, czy też wiktoriański, klient powinien traktować wszystkie w taki sam sposób, za pośrednictwem interfejsu abstrakcyjnego Fotel
. Dzięki temu podejściu, klient wie tylko tyle, że fotele implementują jakąś formę metody usiądźNa
. Ponadto, niezależnie od wariantu zwracanego fotelu, zawsze będą one pasowały do sof lub stolików kawowych jakie produkuje dany obiekt fabryczny.
Pozostaje do wyjaśnienia jeszcze jedna sprawa: jeśli klient ma do czynienia wyłącznie z interfejsami abstrakcyjnymi, to co właściwie tworzy rzeczywiste obiekty fabryczne? Na ogół aplikacja tworzy konkretny obiekt fabryczny na etapie inicjalizacji. Tuż przed tym wybiera stosowny typ fabryki zależnie od konfiguracji lub środowiska.
Struktura
-
Produkty Abstrakcyjne deklarują interfejsy odmiennych produktów, które składają się na wspólną rodzinę.
-
Konkretne Produkty to różnorakie implementacje abstrakcyjnych produktów, pogrupowane według wariantów. Każdy abstrakcyjny produkt (fotel/sofa) musi być zaimplementowany we wszystkich zadanych wariantach (Wiktoriański/Nowoczesny).
-
Interfejs Fabryki Abstrakcyjnej deklaruje zestaw metod służących tworzeniu każdego z abstrakcyjnych produktów.
-
Konkretne Fabryki implementują metody kreacyjne fabryki abstrakcyjnej. Każda konkretna fabryka jest związana z jakimś określonym wariantem produktu i produkuje wyłącznie meble w tym stylu.
-
Mimo, że konkretne fabryki tworzą konkretne egzemplarze produktu, sygnatury ich metod kreacyjnych muszą zwracać stosowne abstrakcyjne produkty. Dzięki temu kod kliencki, który korzysta z fabryki, nie zostanie sprzęgnięty z jakimś konkretnym wariantem produktu, jaki otrzymuje z fabryki. Klient może działać na dowolnym konkretnym wariancie fabryki/produktu, o ile będzie korzystał z interfejsów abstrakcyjnych ich obiektów.
Pseudokod
Poniższy przykład ilustruje zastosowanie Fabryki abstrakcyjnej do tworzenia międzyplatformowych elementów interfejsu użytkownika (UI), unikając tym samym sprzężenia kodu klienckiego z konkretnymi klasami interfejsu użytkownika oraz zachowując zgodność tworzonych elementów UI z danym systemem operacyjnym.
Elementy interfejsu użytkownika tego samego typu powinny zachowywać się podobnie niezależnie od platformy, ale mogą wyglądać nieco inaczej na różnych systemach operacyjnych. Co więcej, to twoim zadaniem jest zapewnić zgodność wizualnego stylu elementów UI ze stylem konkretnego systemu operacyjnego. Nie chcemy przecież wyświetlać kontrolek w stylu macOS w programie uruchamianym pod Windows.
Interfejs fabryki abstrakcyjnej deklaruje pewien zestaw metod kreacyjnych, dzięki którym kod kliencki może tworzyć różne typy elementów interfejsu użytkownika. Konkretne fabryki odpowiadają poszczególnym systemom operacyjnym i tworzą zgodne z nimi wizualnie elementy UI.
Działa to tak: kiedy aplikacja jest uruchamiana, sprawdza pod jakim pracuje systemem operacyjnym. Mając tę wiedzę, aplikacja tworzy obiekt fabryczny z klasy odpowiadającej danemu systemowi. Reszta kodu z kolei korzysta z tej fabryki przy tworzeniu elementów interfejsu użytkownika. Dzięki temu zapobiega się tworzeniu niewłaściwych kontrolek.
Dzięki takiemu podejściu, kod kliencki nie jest zależny od konkretnych klas fabryk oraz elementów UI, pod warunkiem, że będzie korzystał z ich interfejsów abstrakcyjnych. Pozwoli to także kodowi klienckiemu zachować zgodność z fabrykami lub elementami UI mogącymi pojawić się w przyszłości.
Wynikiem takiego projektowania, uniknąć można zmian w kodzie klienckim w razie dodania obsługi nowych stylów wizualnych kontrolek. Wystarczy stworzyć nową klasę fabryczną, która wytwarzać będzie te elementy oraz nieco zmodyfikować kod inicjalizujący aplikacji, aby mógł obrać tę klasę.
Zastosowanie
Stosuj Fabrykę abstrakcyjną, gdy twój kod ma działać na produktach z różnych rodzin, ale jednocześnie nie chcesz, aby ściśle zależał od konkretnych klas produktów. Mogą one bowiem być nieznane na wcześniejszym etapie tworzenia programu, albo chcesz umożliwić przyszłą rozszerzalność aplikacji.
Fabryka abstrakcyjna dostarcza ci interfejs służący tworzeniu obiektów z różnych klas danej rodziny produktów. O ile twój kod będzie kreował obiekty za pośrednictwem tego interfejsu — nie musisz się martwić stworzeniem produktu w niezgodnym z innymi wariancie.
Przemyśl ewentualną implementację wzorca Fabryki abstrakcyjnej, gdy masz do czynienia z klasą posiadającą zestaw Metod wytwórczych które zbytnio przyćmiewają główną odpowiedzialność tej klasy.
W prawidłowo zaprojektowanym programie każda klasa jest odpowiedzialna za jedną rzecz. Gdy zaś klasa ma do czynienia z wieloma typami produktów, warto być może zebrać jej metody wytwórcze i umieścić je w osobnej klasie fabrycznej, albo nawet w pełni zaimplementować Fabrykę abstrakcyjną z ich pomocą.
Jak zaimplementować
-
Stwórz mapę poszczególnych typów produktów z uwzględnieniem wariantów w jakich mogą one być dostępne.
-
Dla każdego typu produktu zaimplementuj abstrakcyjny interfejs. Niech wszystkie konkretne klasy produktów implementują powyższe interfejsy.
-
Zadeklaruj interfejs fabryki abstrakcyjnej zawierający zestaw metod kreacyjnych wszystkich produktów abstrakcyjnych.
-
Zaimplementuj zestaw konkretnych klas fabrycznych — po jednym dla każdego wariantu produktu.
-
Gdzieś w programie umieść kod inicjalizujący fabrykę. Kod ten powinien powołać do życia obiekt jednej z konkretnych klas fabrycznych — zależnie od konfiguracji programu, czy też środowiska, w jakim został uruchomiony. Przekaż następnie ten obiekt fabryczny każdej klasie, której zadaniem jest konstrukcja produktów.
-
Przejrzyj kod aplikacji, wyszukując wszelkie bezpośrednie wywołania konstruktorów produktów. Zamień te wywołania na takie, które odnoszą się do stosownych metod kreacyjnych obiektu fabrycznego.
Zalety i wady
- Zyskujesz pewność, że produkty, jakie otrzymujesz stosując fabrykę, są ze sobą kompatybilne.
- Zapobiegasz ścisłemu sprzęgnięciu konkretnych produktów z kodem klienckim.
- Zasada pojedynczej odpowiedzialności. Możesz zebrać kod kreacyjny produktów w jednym miejscu w programie, ułatwiając tym samym późniejsze utrzymanie kodu.
- Zasada otwarte/zamknięte. Możesz wprowadzać wsparcie dla nowych wariantów produktów bez psucia istniejącego kodu klienckiego.
- Kod może stać się bardziej skomplikowany, niż powinien. Wynika to z konieczności wprowadzenia wielu nowych interfejsów i klas w toku wdrażania tego wzorca projektowego.
Powiązania z innymi wzorcami
-
Wiele projektów zaczyna się od zastosowania Metody wytwórczej (mniej skomplikowanej i dającej się dostosować poprzez tworzenie podklas). Projekty następnie ewoluują stopniowo w Fabrykę abstrakcyjną, Prototyp lub Budowniczego (bardziej elastyczne, ale i bardziej skomplikowane wzorce).
-
Budowniczy koncentruje się na konstruowaniu złożonych obiektów krok po kroku. Fabryka abstrakcyjna specjalizuje się w tworzeniu rodzin spokrewnionych ze sobą obiektów. Fabryka abstrakcyjna zwraca produkt natychmiast, zaś Budowniczy pozwala dołączyć dodatkowe etapy konstrukcji zanim będzie można pobrać finalny produkt.
-
Klasy Fabryka abstrakcyjna często wywodzą się z zestawu Metod wytwórczych, ale można także użyć Prototypu do skomponowania metod w tych klasach.
-
Fabryka abstrakcyjna może służyć jako alternatywa do Fasady gdy jedyne co chcesz zrobić, to ukrycie przed kodem klienckim procesu tworzenia obiektów podsystemu.
-
Fabryka abstrakcyjna może być stosowana wraz z Mostem. Takie sparowanie jest użyteczne gdy niektóre abstrakcje zdefiniowane przez Most mogą współdziałać wyłącznie z określonymi implementacjami. W tym przypadku, Fabryka abstrakcyjna może hermetyzować te relacje i ukryć zawiłości przed kodem klienckim.
-
Fabryki abstrakcyjne, Budowniczych oraz Prototypy można zaimplementować jako Singletony.
Dodatek
- Przeczytaj nasze Porównanie fabryk, jeśli chcesz dowiedzieć się więcej o różnicach pomiędzy poszczególnymi wzorcami fabrycznymi i koncepcjami.