Wiosenna WYPRZEDAŻ

Budowniczy

Znany też jako: Builder

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.

Wzorzec projektowy Budowniczy

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.

Zbyt wiele podklas to kolejny problem.

Program może stać się nadmiernie skomplikowany, jeśli każda możliwa konfiguracja oznacza dodanie nowej podklasy.

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.

Konstruktor teleskopowy

Konstruktor przyjmujący mnóstwo parametrów ma swoją wadę: nie wszystkie parametry będą potrzebne za każdym razem.

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.

Zastosowanie wzorca budowniczy

Wzorzec Budowniczy pozwala konstruować złożone obiekty krok po kroku. Budowniczy ponadto nie pozwala na dostęp do nich innym obiektom, dopóki nie zostaną ukończone.

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.

Różni budowniczowie wykonują to samo zadanie w różny sposób.

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.

Kierownik wie jakie kroki należy wykonać, aby otrzymać działający produkt.

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

Struktura wzorca projektowego BudowniczyStruktura wzorca projektowego Budowniczy
  1. Interfejs Budowniczego deklaruje etapy konstrukcji produktu wspólne dla wszystkich typów budowniczych.

  2. Konkretni Budowniczowie zapewniają różne implementacje etapów konstrukcji. Konkretni budowniczowie mogę tworzyć produkty które nie mają wspólnego interfejsu.

  3. Produkty to powstałe obiekty. Produkty konstruowane przez różnych budowniczych nie muszą należeć do tej samej hierarchii klas, czy interfejsu.

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

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

Struktura przykładu użycia wzorca Budowniczy

Przykład kilkuetapowej konstrukcji samochodów oraz instrukcji obsługi ich modeli.

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.

// Stosowanie wzorca Budowniczy ma sens tylko gdy produkty w
// programie są złożone i wymagają kompleksowej konfiguracji.
// Poniższe dwa produkty są powiązane, ale nie mają wspólnego
// interfejsu.
class Car is
    // Samochód może mieć GPS, komputer pokładowy i pewną liczbę
    // siedzeń. Różne modele samochodów (sportowe, SUV-y,
    // kabriolety) mogą mieć różne opcjonalne funkcjonalności.

class Manual is
    // Do każdego samochodu powinna istnieć instrukcja obsługi,
    // która opisuje jego konfigurację i funkcje.


// Interfejs budowniczy określa metody służące tworzeniu
// poszczególnych części obiektów-produktów.
interface Builder is
    method reset()
    method setSeats(...)
    method setEngine(...)
    method setTripComputer(...)
    method setGPS(...)

// Konkretne klasy budowniczy są zgodne pod względem interfejsu
// i zawierają szczegółowe implementacje etapów budowania. Twój
// program może mieć wiele różnie zaimplementowanych wariacji
// budowniczych.
class CarBuilder implements Builder is
    private field car:Car

    // Nowo utworzona instancja budowniczego powinna zawierać
    // pusty obiekt-produkt, który będzie podstawą do dalszej
    // budowy.
    constructor CarBuilder() is
        this.reset()

    // Metoda resetująca zeruje budowany obiekt.
    method reset() is
        this.car = new Car()

    // Wszystkie etapy produkcji działają na tej samej instancji
    // produktu.
    method setSeats(...) is
        // Ustaw ilość siedzeń w aucie.

    method setEngine(...) is
        // Zamontuj dany silnik.

    method setTripComputer(...) is
        // Zamontuj komputer pokładowy.

    method setGPS(...) is
        // Zamontuj nawigację GPS.

    // Konkretni budowniczy powinni posiadać własne metody
    // pozyskiwania wyników działań, gdyż różni budowniczowie
    // tworzą różne produkty i nie zawsze zgodne pod względem
    // interfejsu. Dlatego też nie można zadeklarować takich
    // metod w interfejsie budowniczego (a przynajmniej nie
    // można tego dokonać w statycznie typowanym języku
    // programowania).
    //
    // Zazwyczaj po zwróceniu wyniku działania klientowi,
    // instancja budowniczego powinna być gotowa na rozpoczęcie
    // produkcji od nowa. Dlatego typową praktyką jest wywołanie
    // metody resetującej na końcu kodu metody `getProduct`. Nie
    // jest to jednak obowiązkowe i można odroczyć zerowanie do
    // momentu gdy klient prześle stosowne polecenie.
    method getProduct():Car is
        product = this.car
        this.reset()
        return product

// W przeciwieństwie do innych wzorców kreacyjnych, budowniczy
// pozwala konstruować produkty które nie mają wspólnego
// interfejsu.
class CarManualBuilder implements Builder is
    private field manual:Manual

    constructor CarManualBuilder() is
        this.reset()

    method reset() is
        this.manual = new Manual()

    method setSeats(...) is
        // Udokumentuj specyfikację siedzeń.

    method setEngine(...) is
        // Dodaj instrukcję silnika.

    method setTripComputer(...) is
        // Dodaj instrukcję komputera pokładowego.

    method setGPS(...) is
        // Dodaj instrukcję nawigacji GPS.

    method getProduct():Manual is
        // Zwróć instrukcję obsługi i zresetuj budowniczego.


// Kierownik jest odpowiedzialny tylko za wywoływanie etapów
// budowy w odpowiedniej kolejności. Przydaje się to gdy
// tworzymy produkty według określonego porządku lub
// konfiguracji. Doprecyzowując — klasa kierownik jest
// opcjonalna, ponieważ klient może kontrolować budowniczych
// bezpośrednio.
class Director is
    // Kierownik może współdziałać z dowolną instancją
    // budowniczego jaką klient mu przekaże. Dzięki temu kod
    // klienta może zmienić ostateczny typ nowo utworzonego
    // produktu. Kierownik może skonstruować wiele wariacji
    // danego produktu postępując według tych samych etapów
    // budowy.
    method constructSportsCar(builder: Builder) is
        builder.reset()
        builder.setSeats(2)
        builder.setEngine(new SportEngine())
        builder.setTripComputer(true)
        builder.setGPS(true)

    method constructSUV(builder: Builder) is
        // ...


// Kod kliencki tworzy obiekt budowniczego, przekazuje go
// kierownikowi a następnie inicjuje proces konstrukcji.
// Ostateczny wynik działania pobiera się od obiektu
// budowniczego.
class Application is

    method makeCar() is
        director = new Director()

        CarBuilder builder = new CarBuilder()
        director.constructSportsCar(builder)
        Car car = builder.getProduct()

        CarManualBuilder builder = new CarManualBuilder()
        director.constructSportsCar(builder)

        // Finalny produkt zwykle pobiera się od obiektu
        // budowniczego, ponieważ kierownik nic o nim nie wie i
        // nie jest zależny od konkretnych budowniczych czy
        // konkretnych produktów.
        Manual manual = builder.getProduct()

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.

class Pizza {
    Pizza(int size) { ... }
    Pizza(int size, boolean cheese) { ... }
    Pizza(int size, boolean cheese, boolean pepperoni) { ... }
    // ...

Tworzenie takich potworów jest możliwe jedynie w językach obsługujących przeciążanie metod, a więc na przykład w C# oraz Javie.

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ć

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

  2. Zadeklaruj te etapy w interfejsie bazowego budowniczego.

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

  4. Rozważ stworzenie klasy kierownika. Może ona zawrzeć różne sposoby konstruowania produktu przy pomocy tego samego obiektu budowniczego.

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

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

Przykłady kodu

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