Wiosenna WYPRZEDAŻ

Metoda wytwórcza

Znany też jako: Konstruktor wirtualny, Virtual constructor, Factory Method

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.

Wzorzec Metody wytwórczej

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.

Dodanie nowej klasy transportu do programu powoduje problem

Dodanie nowej klasy do programu nie jest takie proste, jeśli reszta kodu jest już związana z istniejącymi klasami.

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

Struktura klas kreacyjnych

Podklasy mogą zmieniać klasę obiektów zwracanych przez metodę wytwórczą.

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.

Struktura hierarchii produktów

Wszystkie produkty muszą być zgodne z tym samym 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.

Struktura kodu po zastosowaniu wzorca projektowego Metody wytwórczej

O ile wszystkie klasy produktów implementują wspólny interfejs, możesz przekazywać ich obiekty do kodu klienckiego bez obawy o jego zepsucie.

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

Struktura wzorca Metoda WytwórczaStruktura wzorca Metoda Wytwórcza
  1. Produkt deklaruje interfejs, który jest wspólny dla wszystkich obiektów zwracanych przez twórcę oraz jego podklasy.

  2. Konkretne Produkty są różnymi implementacjami interfejsu produktów.

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

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

Struktura przykładu wzorca projektowego Metody Wytwórczej

Przykład międzyplatformowego okna dialogowego.

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.

// Klasa kreacyjna deklaruje metodę wytwórczą która musi zwracać
// obiekt klasy produktu. Poszczególne podklasy kreatora na ogół
// implementują tę metodę.
class Dialog is
    // Kreator może również posiadać jakąś domyślną
    // implementację metody wytwórczej.
    abstract method createButton():Button

    // Zwróć uwagę, że pomimo swojej nazwy, głównym zadaniem
    // kreatora nie jest tworzenie produktów. Zamiast tego
    // zawiera jakąś kluczową logikę biznesową która jest
    // zależna od obiektów-produktów zwróconych przez metodę
    // wytwórczą. Podklasy mogą pośrednio zmieniać tę logikę
    // biznesową poprzez nadpisywanie metody wytwórczej i
    // zwracanie innych typów produktów.
    method render() is
        // Wywołanie metody wytwórczej w celu stworzenia
        // obiektu-produktu.
        Button okButton = createButton()
        // A następnie użycie produktu.
        okButton.onClick(closeDialog)
        okButton.render()

// Konkretni kreatorzy nadpisują metodę wytwórczą w celu zmiany
// zwracanego typu produktu.
class WindowsDialog extends Dialog is
    method createButton():Button is
        return new WindowsButton()

class WebDialog extends Dialog is
    method createButton():Button is
        return new HTMLButton()

// Interfejs produktu deklaruje wszystkie działania które
// konkretne produkty muszą zaimplementować.
interface Button is
    method render()
    method onClick(f)

// Konkretne produkty posiadają różne implementacje interfejsu
// produktu.
class WindowsButton implements Button is
    method render(a, b) is
        // Renderuj przycisk w stylu Windows.
    method onClick(f) is
        // Powiąż z wbudowanym w system operacyjny zdarzeniem
        // kliknięcia

class HTMLButton implements Button is
    method render(a, b) is
        // Zwróć wersję HTML przycisku.
    method onClick(f) is
        // Powiąż z wbudowanym w przeglądarkę zdarzeniem
        // kliknięcia


class Application is
    field dialog: Dialog

    // Aplikacja wybiera typ kreatora na podstawie bieżącej
    // konfiguracji lub zmiennych środowiskowych.
    method initialize() is
        config = readApplicationConfigFile()

        if (config.OS == "Windows") then
            dialog = new WindowsDialog()
        else if (config.OS == "Web") then
            dialog = new WebDialog()
        else
            throw new Exception("Error! Unknown operating system.")

    // Kod kliencki współpracuje z instancją konkretnego twórcy
    // za pośrednictwem interfejsu bazowego. Tak długo jak
    // klient będzie współpracował z kreatorem za pośrednictwem
    // interfejsu bazowego, można będzie mu przekazywać dowolną
    // podklasę twórcy.
    method main() is
        this.initialize()
        dialog.render()

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:

  1. Najpierw musisz stworzyć jakiś magazyn, który będzie pamiętał wszystkie utworzone obiekty.
  2. Gdy ktoś zgłosi zapotrzebowanie na obiekt, program powinien odszukać wolny obiekt spośród tych już istniejących w puli.
  3. ... i wreszcie zwrócić go kodowi klienckiemu.
  4. 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ć

  1. Wszystkie produkty powinny być zgodne z tym samym interfejsem. Interfejs ten powinien deklarować metody, które są sensowne dla każdego produktu.

  2. Dodaj pustą metodę wytwórczą do klasy kreacyjnej. Zwracany typ tej metody powinien być zgodny z interfejsem wspólnym dla produktów.

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

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

  5. 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 oraz PocztaLądowa. Klasy Transport to: Samolot, Ciężarówka oraz Pociąg. Klasa PocztaLotnicza używa jedynie obiektów klasy Samolot, ale PocztaLądowa zarówno obiektów klasy Ciężarówka, jak i Pociąg. Można więc stworzyć kolejną podklasę (powiedzmy, że PocztaKolejowa) 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 klasie PocztaLądowa który zdecyduje o typie produktów, jakie są potrzebne.

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

Przykłady kodu

Metoda wytwórcza w języku C# Metoda wytwórcza w języku C++ Metoda wytwórcza w języku Go Metoda wytwórcza w języku Java Metoda wytwórcza w języku PHP Metoda wytwórcza w języku Python Metoda wytwórcza w języku Ruby Metoda wytwórcza w języku Rust Metoda wytwórcza w języku Swift Metoda wytwórcza w języku TypeScript

Dodatek

  • Przeczytaj nasze Porównanie fabryk, jeśli masz trudności z rozróżnieniem poszczególnych koncepcji i wzorców wytwórczych.