Budowniczy
Cel
Budowniczy jest kreacyjnym wzorcem projektowym, który daje możliwość tworzenia złożonych obiektów etapami, krok po kroku. Wzorzec ten pozwala produkować różne typy oraz reprezentacje obiektu używając tego samego kodu konstrukcyjnego.
Problem
Wyobraź sobie jakiś skomplikowany obiekt, którego inicjalizacja jest pracochłonnym, wieloetapowym procesem obejmującym wiele pól i obiektów zagnieżdżonych. Taki kod inicjalizacyjny jest często wrzucany do wielgachnego konstruktora, przyjmującego mnóstwo parametrów. Albo jeszcze gorzej: kod taki rozrzucono po całym kodzie klienckim.
Na przykład pomyślmy, jak stworzyć obiekt Dom
. Do zbudowania najprostszego domu wystarczą cztery ściany i podłoga. Do tego drzwi, parę okien i dach. Ale co, jeśli chcesz większy, jaśniejszy dom z podwórkiem i innymi dodatkami (ogrzewanie, kanalizacja, elektryczność)?
Najprostsze rozwiązanie to rozszerzenie klasy bazowej Dom
i stworzenie zestawu podklas, które spełniałyby każdy możliwy zestaw wymogów. Ale takie podejście doprowadzi do wielkiej liczby podklas. Dodanie kolejnego parametru, jak styl werandy, jeszcze bardziej rozbuduje tę hierarchię.
Istnieje jednak inne rozwiązanie, które nie wiąże się z mnożeniem podklas. Można stworzyć jeden wielki konstruktor w klasie bazowej Dom
, uwzględniający wszystkie możliwe parametry, które sterują obiektem typu dom. W ten sposób nie mnożymy liczby klas, ale tworzymy nieco inny problem.
W większości przypadków parametry pozostaną nieużyte, a wywołania konstruktora będą wyglądać niechlujnie. Na przykład tylko niektóre domy mają basen, więc parametry dotyczące basenu w dziewięciu na dziesięć przypadków będą niepotrzebne.
Rozwiązanie
Wzorzec projektowy Budowniczy proponuje ekstrakcję kodu konstrukcyjnego obiektu z jego klasy i umieszczenie go w osobnych obiektach zwanych budowniczymi.
Ten wzorzec projektowy dzieli konstrukcję obiektu na pewne etapy (budujŚciany
, wstawDrzwi
, itd.). Aby powołać do życia obiekt, wykonuje się ciąg takich etapów za pośrednictwem obiektu-budowniczego. Istotne jest to, że nie musisz wywoływać wszystkich etapów. Możesz bowiem ograniczyć się tylko do tych kroków, które są niezbędne do określenia potrzebnej nam konfiguracji obiektu.
Niektóre etapy konstrukcji mogą wymagać odmiennych implementacji, zależnie od potrzebnej w danej chwili reprezentacji produktu. Na przykład, ściany leśnej chatki mogą być drewniane, ale mury zamku warownego — kamienne.
W takim przypadku, można utworzyć wiele różnych klas budowniczych które implementują te same etapy konstrukcji, ale w różny sposób. Można następnie korzystać z tych budowniczych podczas procesu konstrukcji (np. odpowiednia kolejność wywołań etapów budowy) aby wytworzyć różne rodzaje obiektów.
Przykładowo, wyobraź sobie budowniczego, który konstruuje wyłącznie z drewna i szkła, drugi zaś stosuje tylko kamień i żelazo, a trzeci — złoto i diamenty. Wywołując te same etapy, uzyskasz zwykły dom autorstwa pierwszego budowniczego, drugi z nich zbuduje mały zamek, a trzeci — pałac. Jednakże, to zadziała tylko pod warunkiem, że kod kliencki, który wywołuje etapy budowy, jest w stanie komunikować się z budowniczymi za pośrednictwem wspólnego interfejsu.
Kierownik
Można pójść jeszcze dalej i przenieść kolejkę bezpośrednich wywołań budowniczego do osobnej klasy, zwanej kierownikiem. Kierownik określa kolejność etapów jaką musi zachować budowniczy, który z kolei implementuje te etapy konstrukcji obiektu.
Posiadanie w programie klasy kierownika nie jest niezbędne. Można bowiem zawsze wywoływać etapy konstrukcji w odpowiedniej kolejności z poziomu kodu klienckiego. Jednakże, kierownik może okazać się dobrym miejscem na umieszczenie czynności konstrukcyjnych, potrzebnych w innych miejscach programu.
Dodatkowo, klasa kierownik ukrywa szczegóły konstrukcji produktu przed kodem klienckim. Klient musi tylko skojarzyć budowniczego z kierownikiem, wywołać proces budowy za pośrednictwem tego pierwszego, a następnie odebrać wynik pracy od drugiego.
Struktura
-
Interfejs Budowniczego deklaruje etapy konstrukcji produktu wspólne dla wszystkich typów budowniczych.
-
Konkretni Budowniczowie zapewniają różne implementacje etapów konstrukcji. Konkretni budowniczowie mogę tworzyć produkty które nie mają wspólnego interfejsu.
-
Produkty to powstałe obiekty. Produkty konstruowane przez różnych budowniczych nie muszą należeć do tej samej hierarchii klas, czy interfejsu.
-
Klasa Kierownik definiuje kolejność w jakiej należy wywołać etapy konstrukcyjne, aby móc stworzyć i następnie użyć ponownie określone konfiguracje produktów.
-
Klient musi dopasować jeden z obiektów budowniczych do kierownika. Zazwyczaj robi się to tylko raz, za pośrednictwem parametru przekazywanego do konstruktora kierownika. Następnie kierownik za pomocą obiektu budowniczego wykonuje dalszą konstrukcję. Jednakże istnieje alternatywne podejście w przypadku przekazania obiektu budowniczego metodzie produkcyjnej kierownika. W takim przypadku kierownik może skorzystać z różnych budowniczych.
Pseudokod
Poniższy przykład użycia wzorca projektowego Budowniczy pokazuje, jak można ponownie wykorzystać ten sam kod konstrukcyjny obiektu budując różne typy produktów, takie jak samochody oraz odpowiednie do nich instrukcje obsługi.
Samochód jest skomplikowanym obiektem, który można skonstruować na setki sposobów. Zamiast obciążać klasę Samochód
olbrzymim konstruktorem, wyekstrahowaliśmy kod montażu auta do osobnej klasy budowniczego samochodu. Klasa ta ma zestaw metod pozwalających skonfigurować dowolną część auta.
Jeśli kod kliencki musi utworzyć specjalny model samochodu na zamówienie, może skorzystać bezpośrednio z budowniczego. Z drugiej strony, klient może oddelegować montaż klasie kierownika, która wie jak za pomocą budowniczego skonstruować wiele najpopularniejszych modeli aut.
Może cię to zaskoczyć, ale do każdego auta powinna istnieć instrukcja obsługi (poważnie? ktoś je czyta?). Instrukcje opisują cechy i wyposażenie samochodów, więc ich zawartość będzie się różnić pomiędzy modelami. Dlatego warto ponownie wykorzystywać istniejący proces konstrukcyjny zarówno dla aut, jak i dla ich instrukcji. Oczywiście tworzenie instrukcji to nie to samo, co montaż auta i dlatego musimy dodać kolejną klasę budowniczego specjalizującą się w tworzeniu instrukcji. Klasa ta implementuje takie same metody budowy, co jej montująca auta krewna, ale zamiast montować — opisuje. Poprzez przekazanie tych budowniczych do tego samego obiektu kierownika, konstruujemy albo pojazd, albo instrukcję obsługi.
Ostatni etap to pobranie nowo utworzonego obiektu. Metalowy samochód i papierowa instrukcja obsługi, to jednak bardzo różne rzeczy, choć ze sobą związane. Nie możemy umieścić metody pobierającej wynik pracy w kierowniku, zanim nie powiążemy go z konkretną klasą produktów. Dlatego też odbieramy wynik u budowniczego, który jest jego autorem.
Zastosowanie
Stosuj wzorzec Budowniczy, aby pozbyć się “teleskopowych konstruktorów”.
Załóżmy, że masz konstruktor, przyjmujący 10 opcjonalnych parametrów. Wywołanie takiego potwora jest co najmniej niewygodne, dlatego przeciążamy konstruktor i tworzymy wiele jego krótszych wersji, wymagających mniej parametrów. Będą one wciąż odwoływać się do głównego konstruktora, przekazując jakieś domyślne wartości w miejsce pominiętych argumentów.
Wzorzec Budowniczy pozwala konstruować obiekty krok po kroku, w miarę jak staje się to w programie potrzebne. Po zaimplementowaniu tego wzorca, nie musisz przekazywać konstruktorowi tuzina parametrów.
Stosuj wzorzec Budowniczy, gdy potrzebujesz możliwości tworzenia różnych reprezentacji jakiegoś produktu (na przykład, domy z kamienia i domy z drewna).
Wzorzec Budowniczy można użyć gdy konstruowanie różnorakich reprezentacji produktu obejmuje podobne etapy, które różnią się jedynie szczegółami.
Bazowy interfejs budowniczego definiuje wszelkie możliwe etapy konstrukcji, a konkretni budowniczy implementują te kroki by móc tworzyć poszczególne reprezentacje obiektów. Natomiast klasa kierownik pilnuje właściwego porządku konstruowania.
Stosuj ten wzorzec do konstruowania drzew Kompozytowych lub innych złożonych obiektów.
Wzorzec budowniczego umożliwia konstrukcję w etapach. Niektóre z nich możemy odroczyć bez szkody dla finalnego produktu. Możemy nawet wywoływać etapy rekursywnie, co przydaje się przy budowie drzewa obiektów.
Budowniczy uniemożliwia dostęp do nieskończonego produktu przez okres jego konstrukcji. Zapobiega to pozyskiwaniu niekompletnych wyników przez kod kliencki.
Jak zaimplementować
-
Upewnij się, że jesteś w stanie zdefiniować konkretne, wspólne etapy, wykonywane przy tworzeniu wszystkich dostępnych reprezentacji produktu. Bez tego nie uda się wdrożyć tego wzorca.
-
Zadeklaruj te etapy w interfejsie bazowego budowniczego.
-
Stwórz konkretną klasę budowniczego dla każdej reprezentacji produktu i zaimplementuj specyficzne dla nich etapy konstrukcyjne.
Nie zapomnij zaimplementować metodę pobierającą wynik konstrukcji. Powodem, dla którego taka metoda nie może być zadeklarowana w ramach interfejsu budowniczego jest to, że różni budowniczowie mogą tworzyć obiekty które nie posiadają wspólnego interfejsu. Dlatego też nie byłoby wiadomo jaki typ obiektu zwracałaby taka metoda. Jednakże, jeśli masz do czynienia wyłącznie z produktami wchodzącymi w skład jednej hierarchii, metodę taką można bezpiecznie dodać do interfejsu bazowego.
-
Rozważ stworzenie klasy kierownika. Może ona zawrzeć różne sposoby konstruowania produktu przy pomocy tego samego obiektu budowniczego.
-
Kod kliencki tworzy zarówno obiekty budowniczego, jak i kierownika. Przed rozpoczęciem konstrukcji, klient musi przekazać kierownikowi obiekt budowniczego. Zazwyczaj klient musi to zrobić jednorazowo, za pośrednictwem parametrów konstruktora kierownika. Kierownik potem wykonuje wszelkie prace konstrukcyjne za pomocą tego budowniczego. Jest też inny sposób, w którym budowniczy jest przekazywany bezpośrednio metodzie konstrukcyjnej kierownika.
-
Wynik konstrukcji może być odebrany bezpośrednio od kierownika tylko wtedy, gdy wszystkie produkty współdzielą taki sam interfejs. W przeciwnym razie, klient powinien pobrać wynik od budowniczego.
Zalety i wady
- Możesz konstruować obiekty etapami, odkładać niektóre etapy, lub wykonywać je rekursywnie.
- Możesz wykorzystać ponownie ten sam kod konstrukcyjny budując kolejne reprezentacje produktów.
- Zasada pojedynczej odpowiedzialności. Można odizolować skomplikowany kod konstrukcyjny od logiki biznesowej produktu.
- Kod staje się bardziej skomplikowany, gdyż wdrożenie tego wzorca wiąże się z dodaniem wielu nowych klas.
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.
-
Możesz zastosować wzorzec Budowniczy by tworzyć złożone drzewa Kompozytowe dzięki możliwości zaprogramowania ich etapów konstrukcji tak, aby odbywały się rekurencyjnie.
-
Możliwe jest połączenie wzorców Budowniczy i Most: klasa kierownik pełni rolę abstrakcji, zaś poszczególni budowniczy stanowią implementacje.
-
Fabryki abstrakcyjne, Budowniczych oraz Prototypy można zaimplementować jako Singletony.