Metoda wytwórcza
Cel
Metoda wytwórcza jest kreacyjnym wzorcem projektowym, który udostępnia interfejs do tworzenia obiektów w ramach klasy bazowej, ale pozwala podklasom zmieniać typ tworzonych obiektów.
Problem
Wyobraź sobie, że tworzysz aplikację do zarządzania logistyką. Pierwsza wersja twojej aplikacji pozwala jedynie na obsługę transportu za pośrednictwem ciężarówek, więc większość kodu znajduje się wewnątrz klasy Ciężarówka
.
Po jakimś czasie twoja aplikacja staje się całkiem popularna. Codziennie otrzymujesz tuzin próśb od firm realizujących spedycję morską, abyś dodał stosowną funkcjonalność do swej aplikacji.
Świetna wiadomość, prawda? Ale co z kodem? W tej chwili większość twojego kodu jest powiązana z klasą Ciężarówka
. Dodanie do aplikacji klasy Statki
wymagałoby dokonania zmian w całym kodzie. Co więcej, jeśli później zdecydujesz się dodać kolejny rodzaj transportu, zapewne będziesz musiał dokonać tych zmian jeszcze jeden raz.
Rezultatem powyższych działań będzie brzydki kod, pełen instrukcji warunkowych, których zadaniem będzie dostosowanie zachowania aplikacji zależnie od klasy transportu.
Rozwiązanie
Wzorzec projektowy Metody wytwórczej proponuje zamianę bezpośrednich wywołań konstruktorów obiektów (wykorzystujących operator new
) na wywołania specjalnej metody wytwórczej. Jednak nie przejmuj się tym: obiekty nadal powstają za pośrednictwem operatora new
, ale teraz dokonuje się to za kulisami — z wnętrza metody wytwórczej. Obiekty zwracane przez metodę wytwórczą często są nazywane produktami.
Na pierwszy rzut oka zmiana ta może wydawać się bezcelowa. Przecież przenieśliśmy jedynie wywołanie konstruktora z jednej części programu do drugiej. Ale zwróć uwagę, że teraz możesz nadpisać metodę wytwórczą w podklasie, a tym samym zmienić klasę produktów zwracanych przez metodę.
Istnieje jednak małe ograniczenie: podklasy mogą zwracać różne typy produktów tylko wtedy, gdy produkty te mają wspólną klasę bazową lub wspólny interfejs. Ponadto, zwracany typ metody wytwórczej w klasie bazowej powinien być zgodny z tym interfejsem.
Na przykład zarówno klasy Ciężarówka
, jak i Statek
powinny implementować interfejs Transport
, który z kolei deklaruje metodę dostarczaj
. Każda klasa różnie implementuje tę metodę: ciężarówki dostarczają towar drogą lądową, statki drogą morską. Metoda wytwórcza znajdująca się w klasie LogistykaDrogowa
zwraca obiekty Ciężarówka
, zaś metoda wytwórcza w klasie LogistykaMorska
zwraca Statki
.
Kod, który wykorzystuje metodę wytwórczą (zwany często kodem klienckim) nie widzi różnicy pomiędzy faktycznymi produktami zwróconymi przez różne podklasy. Klient traktuje wszystkie produkty jako abstrakcyjnie pojęty Transport
. Klient wie także, że wszystkie obiekty transportowe posiadają metodę dostarczaj
, ale szczegóły jej działania nie są dla niego istotne.
Struktura
-
Produkt deklaruje interfejs, który jest wspólny dla wszystkich obiektów zwracanych przez twórcę oraz jego podklasy.
-
Konkretne Produkty są różnymi implementacjami interfejsu produktów.
-
Klasa Twórca deklaruje metodę wytwórczą, która zwraca nowe obiekty-produkty. Istotne jest, że typ zwracany przez tę metodę jest zgodny z interfejsem produktu.
Możesz zadeklarować metodę wytwórczą jako abstrakcyjną. Wówczas każda podklasa będzie musiała zaimplementować swoją jej wersję. Innym sposobem jest sprawienie, aby bazowa metoda wytwórcza zwracała jakiś domyślny typ produktu.
Weź jednak pod uwagę, że wbrew swojej nazwie, tworzenie produktów nie jest główną odpowiedzialnością Klasy Twórcy. Zazwyczaj klasa kreacyjna zawiera już jakąś ważną logikę biznesową związaną z produktami. Metoda wytwórcza pomaga rozprzęgnąć tę logikę i konkretne klasy produktów. Oto analogia: duża firma tworząca oprogramowanie może posiadać swój dział szkoleniowy dla programistów. Ale głównym zadaniem firmy jako całości jest nadal tworzenie kodu, a nie tworzenie programistów.
-
Konkretni Twórcy nadpisują bazową metodę wytwórczą, co sprawia, że zwraca ona inny typ obiektu.
Tu jednak uwaga: Metoda wytwórcza nie musi wciąż tworzyć nowych instancji. Może też zwrócić istniejący już obiekt z pamięci podręcznej, puli obiektów lub z innego źródła.
Pseudokod
Przykład ten ilustruje, jak Metoda Wytwórcza może służyć tworzeniu elementów interfejsu użytkownika (UI) które będą przenośne między platformami. Nie występuje tu sprzęgnięcie kodu klienckiego z konkretnymi klasami interfejsu użytkownika.
Bazowa klasa okna dialogowego wykorzystuje różne elementy interfejsu użytkownika rysując na ekranie okno. W różnych systemach operacyjnych, elementy te mogą różnić się nieco wyglądem, ale powinny zachowywać się w sposób spójny. Przycisk w Windows powinien być wciąż przyciskiem również w Linux.
Wraz z pojawieniem się metody wytwórczej, znika konieczność przepisywania logiki okna dialogowego dla poszczególnych systemów operacyjnych. Jeśli zadeklarujemy metodę wytwórczą, która tworzy przyciski w ramach klasy bazowej okna dialogowego, możemy później stworzyć dodatkową podklasę okna dialogowego, która będzie zwracała przyciski w stylu Windows z poziomu metody wytwórczej. Podklasa odziedziczy wówczas większość kodu okna z klasy bazowej, ale dzięki metodzie wytwórczej, będzie renderowała przyciski w stylu Windows.
Aby ten wzorzec zadziałał, klasa bazowa okna dialogowego musi działać korzystając z przycisków abstrakcyjnych klasy bazowej lub interfejsu, zgodnie z którym powstaną wszystkie konkretne przyciski. Dzięki temu, kod okna dialogowego pozostanie funkcjonalny niezależnie od tego, jaki będzie rodzaj przycisku.
Oczywiście możesz zastosować to podejście również w stosunku do innych elementów interfejsu użytkownika. Jednak wraz z dodaniem do okna dialogowego każdej kolejnej metody wytwórczej, zbliżasz się do wzorca projektowego zwanego Fabryka abstrakcyjna. Ale nie obawiaj się, później zajmiemy się również tym problemem.
Zastosowanie
Stosuj Metodę Wytwórczą gdy nie wiesz z góry jakie typy obiektów pojawią się w twoim programie i jakie będą między nimi zależności.
Metoda Wytwórcza oddziela kod konstruujący produkty od kodu który faktycznie z tych produktów korzysta. Dlatego też łatwiej jest rozszerzać kod konstruujący produkty bez konieczności ingerencji w resztę kodu.
Przykładowo, aby dodać nowy typ produktu do aplikacji, będziesz musiał utworzyć jedynie podklasę kreacyjną i nadpisać jej metodę wytwórczą.
Korzystaj z Metody Wytwórczej gdy zamierzasz pozwolić użytkującym twą bibliotekę lub framework rozbudowywać jej wewnętrzne komponenty.
Dziedziczenie jest prawdopodobnie najłatwiejszym sposobem rozszerzania domyślnego zachowania się biblioteki lub frameworku. Ale skąd framework wiedziałby o konieczności zastosowania twojej podklasy, zamiast standardowego komponentu?
Rozwiązaniem jest zredukowanie kodu konstruującego komponenty na przestrzeni frameworku do pojedynczej metody wytwórczej. Trzeba też umożliwić nadpisywanie tej metody, a nie tylko rozbudowywanie samego komponentu.
Sprawdźmy, jak by to wyglądało. Wyobraź sobie, że piszesz aplikację korzystając z open source’owego frameworku interfejsu użytkownika (UI). Twoja aplikacja ma posiadać okrągłe przyciski, ale framework oferuje jedynie prostokątne. Rozszerzasz więc standardową klasę Przycisk
o podklasę OkrągłyPrzycisk
. Ale teraz trzeba również sprawić, by główna klasa UIFramework
korzystała z nowo utworzonej podklasy, zamiast z domyślnej.
Aby to osiągnąć, tworzysz podklasę UIZOkrągłymiPrzyciskami
z bazowej klasy frameworku i nadpisujesz jej metodę stwórzPrzycisk
. Metoda ta będzie zwracać obiekty klasy Przycisk
w klasie bazowej, zaś twoja jej podklasa zwróci obiekty OkrągłyPrzycisk
. Od teraz skorzystasz z klasy UIZOkrągłymiPrzyciskami
zamiast klasy UIFramework
. I to tyle!
Korzystaj z Metody wytwórczej gdy chcesz oszczędniej wykorzystać zasoby systemowe poprzez ponowne wykorzystanie już istniejących obiektów, zamiast odbudowywać je raz za razem.
Powyższa sytuacja może wyłonić się na pierwszy plan, gdy mamy do czynienia z dużymi obiektami, wymagającymi sporej ilości zasobów. Mogą do nich należeć połączenia do bazy danych, systemy plików oraz zasoby sieciowe.
Zastanówmy się, co musi się stać, aby istniejący obiekt mógł zostać wykorzystany ponownie:
- Najpierw musisz stworzyć jakiś magazyn, który będzie pamiętał wszystkie utworzone obiekty.
- Gdy ktoś zgłosi zapotrzebowanie na obiekt, program powinien odszukać wolny obiekt spośród tych już istniejących w puli.
- ... i wreszcie zwrócić go kodowi klienckiemu.
- Jeśli nie ma żadnych wolnych obiektów, program powinien utworzyć nowy (i dodać go do puli magazynowej).
To strasznie dużo kodu! I na dodatek cały musi się znaleźć w jednym miejscu, aby uniknąć rozrzucania jego kopii po całym programie.
Prawdopodobnie, najbardziej oczywistym i odpowiednim miejscem na umieszczenie tego nowego kodu jest konstruktor tej klasy, której obiekty chcemy ponownie wykorzystywać. Ale konstruktor musi z definicji zawsze zwracać nowe obiekty. Nie może zwracać istniejących jego instancji.
Dlatego też będzie potrzebna zwykła metoda, która zdolna jest zarówno tworzyć nowe obiekty, jak i pozwolić na wykorzystanie już istniejących. A to już brzmi jak metoda wytwórcza.
Jak implementować
-
Wszystkie produkty powinny być zgodne z tym samym interfejsem. Interfejs ten powinien deklarować metody, które są sensowne dla każdego produktu.
-
Dodaj pustą metodę wytwórczą do klasy kreacyjnej. Zwracany typ tej metody powinien być zgodny z interfejsem wspólnym dla produktów.
-
W kodzie kreacyjnym należy odnaleźć wszystkie odniesienia do konstruktorów produktów. Jeden po drugim, trzeba zamienić je na wywołania metody wytwórczej, ekstrahując przy tym kod kreacyjny produktów, aby umieścić go w metodzie wytwórczej.
Możliwe, że konieczne okaże się ustanowienie tymczasowego parametru w metodzie wytwórczej, aby kontrolować jaki typ produktu będzie zwrócony.
Na tym etapie, kod metody wytwórczej może brzydko wyglądać. Może się na przykład okazać, że wewnątrz znajduje się wielki operator
switch
wybierający stosowną klasę produktu, jaką należy powołać do istnienia. Ale nie martw się, niedługo się tym zajmiemy. -
Teraz stwórz zestaw podklas kreacyjnych dla każdego typu produktu wymienionego w metodzie wytwórczej. Nadpisz metodę wytwórczą w każdej z podklas i wyekstrahuj stosowne fragmenty kodu konstruującego z metody bazowej.
-
Jeśli mamy zbyt wiele typów produktów i nie ma sensu tworzyć podklasy dla każdego z nich, możesz wykorzystać ponownie parametr kontrolny klasy bazowej w podklasach.
Na przykład wyobraź sobie, że masz następującą hierarchię klas. Istnieje klasa bazowa
Poczta
z kilkoma podklasami:PocztaLotnicza
orazPocztaLądowa
. KlasyTransport
to:Samolot
,Ciężarówka
orazPociąg
. KlasaPocztaLotnicza
używa jedynie obiektów klasySamolot
, alePocztaLądowa
zarówno obiektów klasyCiężarówka
, jak iPociąg
. Można więc stworzyć kolejną podklasę (powiedzmy, żePocztaKolejowa
) aby obsłużyć oba przypadki, ale istnieje lepszy sposób. Kod kliencki może bowiem przekazać parametr metodzie wytwórczej znajdującej się w klasiePocztaLądowa
który zdecyduje o typie produktów, jakie są potrzebne. -
Jeśli po dokonaniu wszystkich ekstrakcji, bazowa metoda wytwórcza została pusta, możesz uczynić ją abstrakcyjną. Jeśli jednak coś w niej pozostało, możesz pozostawić to jako domyślne zachowanie się metody.
Zalety i wady
- Unikasz ścisłego sprzęgnięcia pomiędzy twórcą a konkretnymi produktami.
- Zasada pojedynczej odpowiedzialności. Możesz przenieść kod kreacyjny produktów w jedno miejsce programu, ułatwiając tym samym utrzymanie kodu.
- Zasada otwarte/zamknięte. Możesz wprowadzić do programu nowe typy produktów bez psucia istniejącego kodu klienckiego.
- Kod może się skomplikować, ponieważ aby zaimplementować wzorzec, musisz utworzyć liczne podklasy. W najlepszej sytuacji wprowadzisz ów wzorzec projektowy do już istniejącej hierarchii klas kreacyjnych.
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).
-
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.
-
Możesz zastosować Metodę wytwórczą wraz z Iteratorem aby pozwolić podklasom kolekcji zwracać różne typy iteratorów kompatybilnych z kolekcją.
-
Prototyp nie bazuje na dziedziczeniu, więc nie posiada właściwych temu podejściu wad. Z drugiej strony jednak Prototyp wymaga skomplikowanej inicjalizacji klonowanego obiektu. Metoda wytwórcza bazuje na dziedziczeniu, ale nie wymaga etapu inicjalizacji.
-
Metoda wytwórcza to wyspecjalizowana Metoda szablonowa. Może stanowić także jeden z etapów większej Metody szablonowej.
Dodatek
- Przeczytaj nasze Porównanie fabryk, jeśli masz trudności z rozróżnieniem poszczególnych koncepcji i wzorców wytwórczych.