Wiosenna WYPRZEDAŻ

Fabryka abstrakcyjna

Znany też jako: Abstract Factory

Cel

Fabryka abstrakcyjna jest kreacyjnym wzorcem projektowym, który pozwala tworzyć rodziny spokrewnionych ze sobą obiektów bez określania ich konkretnych klas.

Wzorzec fabryki abstrakcyjnej

Problem

Wyobraź sobie, że tworzysz symulator sklepu meblowego. Twój kod składa się z klas, które reprezentują:

  1. Rodzinę spokrewnionych produktów, powiedzmy: Fotel + Sofa + StolikKawowy.

  2. Różne warianty w ramach powyższej rodziny. Na przykład, produkty Fotel + Sofa są dostępne w wariantach: Nowoczesny, Wiktoriański, ArtDeco.

Rodziny produktów i ich warianty.

Rodziny produktów i ich warianty.

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.

Sofa zaprojektowana w stylu nowoczesnym nie pasuje do foteli wiktoriańskich.

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.

Hierarchia klas Foteli

Wszystkie warianty tego samego obiektu muszą się znaleźć w jednej hierarchii klasowej.

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.

Hierarchia klas _Fabrycznych_

Każda konkretna fabryka odpowiada konkretnemu wariantowi produktu.

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.

Klienta nie powinno obchodzić to, z jaką konkretnie klasą fabryczną ma do czynienia.

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

Wzorzec projektowy Fabryki abstrakcyjnejWzorzec projektowy Fabryki abstrakcyjnej
  1. Produkty Abstrakcyjne deklarują interfejsy odmiennych produktów, które składają się na wspólną rodzinę.

  2. 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).

  3. Interfejs Fabryki Abstrakcyjnej deklaruje zestaw metod służących tworzeniu każdego z abstrakcyjnych produktów.

  4. 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.

  5. 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.

Diagram klas przykładu użycia wzorca Fabryki abstrakcyjnej

Przykład międzyplatformowych klas UI.

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ę.

// Interfejs fabryki abstrakcyjnej deklaruje pewien zestaw metod
// które zwracają różne produkty abstrakcyjne. Produkty te
// łącznie nazywa się rodziną i łączy je jakiś wysokopoziomowy
// wspólny motyw lub koncepcja. Produkty z jednej rodziny są
// zwykle zdolne do współpracy z sobą nawzajem. Rodzina
// produktów może mieć wiele wariantów, ale produkty jednego
// wariantu są niekompatybilne z produktami z rodziny innego
// wariantu.
interface GUIFactory is
    method createButton():Button
    method createCheckbox():Checkbox

// Konkretne fabryki tworzą produkty należące do jednego
// wariantu jednej rodziny. Fabryka gwarantuje że powstałe
// produkty będą kompatybilne. Zwracane typy w sygnaturach metod
// wytwórczych konkretnej fabryki określone są jako produkty
// abstrakcyjne, zaś konkretna instancja produktu powstaje
// wewnątrz metody.
class WinFactory implements GUIFactory is
    method createButton():Button is
        return new WinButton()
    method createCheckbox():Checkbox is
        return new WinCheckbox()

// Każda konkretna fabryka ma swój wariant produktu.
class MacFactory implements GUIFactory is
    method createButton():Button is
        return new MacButton()
    method createCheckbox():Checkbox is
        return new MacCheckbox()

// Każdy odrębny produkt z rodziny powinien mieć interfejs
// bazowy. Wszystkie warianty produktu muszą zaimplementować ten
// interfejs.
interface Button is
    method paint()

// Konkretne fabryki mają swoje konkretne produkty.
class WinButton implements Button is
    method paint() is
        // Renderuj przycisk w stylu Windows.

class MacButton implements Button is
    method paint() is
        // Renderuj przycisk w stylu macOS.

// Oto interfejs bazowy innego produktu. Wszystkie produkty mogą
// ze sobą współdziałać, ale właściwa interakcja możliwa jest
// wyłącznie pomiędzy produktami jednego konkretnego wariantu.
interface Checkbox is
    method paint()

class WinCheckbox implements Checkbox is
    method paint() is
        // Renderuj pole wyboru w stylu Windows.

class MacCheckbox implements Checkbox is
    method paint() is
        // Renderuj pole wyboru w stylu macOS.

// Kod kliencki współpracuje z fabrykami i produktami wyłącznie
// stosując typy abstrakcyjne: GUIFactory, Button i Checkbox.
// Pozwala to przekazywać klientowi dowolną podklasę produktu
// lub fabryki.
class Application is
    private field factory: GUIFactory
    private field button: Button
    constructor Application(factory: GUIFactory) is
        this.factory = factory
    method createUI() is
        this.button = factory.createButton()
    method paint() is
        button.paint()

// Aplikacja wybiera typ fabryki na podstawie bieżącej
// konfiguracji lub zmiennych środowiskowych i tworzy jej
// instancję w trakcie działania programu (zazwyczaj na etapie
// inicjalizacji).
class ApplicationConfigurator is
    method main() is
        config = readApplicationConfigFile()

        if (config.OS == "Windows") then
            factory = new WinFactory()
        else if (config.OS == "Mac") then
            factory = new MacFactory()
        else
            throw new Exception("Error! Unknown operating system.")

        Application app = new Application(factory)

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ć

  1. Stwórz mapę poszczególnych typów produktów z uwzględnieniem wariantów w jakich mogą one być dostępne.

  2. Dla każdego typu produktu zaimplementuj abstrakcyjny interfejs. Niech wszystkie konkretne klasy produktów implementują powyższe interfejsy.

  3. Zadeklaruj interfejs fabryki abstrakcyjnej zawierający zestaw metod kreacyjnych wszystkich produktów abstrakcyjnych.

  4. Zaimplementuj zestaw konkretnych klas fabrycznych — po jednym dla każdego wariantu produktu.

  5. 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.

  6. 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.

Przykłady kodu

Fabryka abstrakcyjna w języku C# Fabryka abstrakcyjna w języku C++ Fabryka abstrakcyjna w języku Go Fabryka abstrakcyjna w języku Java Fabryka abstrakcyjna w języku PHP Fabryka abstrakcyjna w języku Python Fabryka abstrakcyjna w języku Ruby Fabryka abstrakcyjna w języku Rust Fabryka abstrakcyjna w języku Swift Fabryka abstrakcyjna w języku TypeScript

Dodatek

  • Przeczytaj nasze Porównanie fabryk, jeśli chcesz dowiedzieć się więcej o różnicach pomiędzy poszczególnymi wzorcami fabrycznymi i koncepcjami.