W dzisiejszym wpisie chciałbym skupić się na wzorcu projektowym Budowniczy. Ponieważ poprzednio rozpisałem się troszkę o gotowaniu i mojej umiarkowanej skłonności do gubienia kilogramów, zatem na warsztat weźmiemy sobie restaurację serwującą burgery 🙂

Zanim zaczniemy podawać klientom nasze równie smaczne, co niezdrowe kanapki, najpierw skupmy się na przepisie na nasz specjał, utwórzmy prostą klasę Burger:

<?php

class Burger 
{
    private string $size;
    private string $meat;
    private bool $cheese = false;
    private bool $pepperoni = false;
    private bool $lettuce = false;
    private bool $tomato = false;
    private array $sauces = [];

    // Konstruktor przyjmuje parametry Burgera, musimy podać rozmiar i typ mięsa, reszta parametrów ma wartości domyślne
    public function __construct(
        string $size, 
        string $meat, 
        bool $cheese = false, 
        bool $pepperoni = false, 
        bool $lettuce = false, 
        bool $tomato = false, 
        array $sauces = []
    ) {
        $this->size = $size;
        $this->meat = $meat;
        $this->cheese = $cheese;
        $this->pepperoni = $pepperoni;
        $this->lettuce = $lettuce;
        $this->tomato = $tomato;
        $this->sauces = $sauces;
    }

    public function describe(): void 
    {
        echo "Burger ({$this->size}) z: {$this->meat}";
        if ($this->cheese) echo ", ser";
        if ($this->pepperoni) echo ", pepperoni";
        if ($this->lettuce) echo ", sałata";
        if ($this->tomato) echo ", pomidor";
        if (!empty($this->sauces)) echo ", sosy: " . implode(', ', $this->sauces);
        echo ".\n";
    }
}

Spróbujmy teraz przyrządzić kilka burgerów, zacznijmy od najskromniejszej wersji, składającej się z pieczywa i mięsa:

$worldSimplestBurger = new Burger('XL', 'Wołowina');

W powyższym przypadku nie ma żadnego problemu, ponieważ podajemy tylko rozmiar i typ mięsa, cała reszta parametrów posiada wartości domyślne, które zostaną zdefiniowane przy tworzeniu obiektu, jednak co się stanie, jeśli zechcemy zamówić coś na wypasie? Przekażemy do konstruktora wszystkie możliwe argumenty:

$luxuryBurger = new Burger('L', 'Wołowina', true, true, true, true, ['BBQ', 'Czosnkowy']);

Mamy tutaj wszystko, czego dusza zapragnie: mięso, ser, pepperoni, sałatę, pomidor i dwa sosy, jednak mimo wszystko odnosimy wrażenie, że używanie naszego konstruktora robi się mało czytelne. Nie wiem, jak czytelnik, ale ja już zgubiłem się i nie wiem, które „true” za co odpowiada.

Dalej jest jeszcze gorzej, co jeśli odwiedzi nas klient, który lubi samo mięso w ostrej otoczce? Oto nasz ostry burger:

$fireMeatBurger = new Burger('L', 'Kurczak', false, true, false, false, ['Sriracha']);

Pomimo faktu, iż do domyślnej konfiguracji naszego burgera dodajemy tylko pepperoni oraz ostry sos Sriracha, nadal musimy podać pozostałe wartości parametrów. W przeciwnym wypadku niechcący dodalibyśmy ser albo warzywa, a to skończyłoby się reklamacją wkurzonego klienta.

Mamy też inny problem, co jeśli w przyszłości rozwiniemy naszą ofertę i zechcemy dodać do naszej klasy Burger nowe składniki, np. bekon albo ogórek? Musimy dopisać je na końcu (po sosach) i mamy wizualny bałagan, albo wpiszemy je do środka i ryzykujemy chaosem wśród istniejących obiektów (np. burger z sałatą nagle stanie się burgerem z bekonem).

Jak sobie z tym poradzić, tutaj uratuje nas wzorzec Budowniczy. Zacznijmy od planu taktycznego, czyli diagramu UML:

Wzorzec Budowniczy przydaje się idealnie, gdy mamy obiekty z ogromną ilością atrybutów, jednak potrzebujemy zmodyfikować tylko niewielką ich część przed utworzeniem obiektu. Przede wszystkim poprawia to czytelność kodu oraz zdejmuje z nas konieczność wywoływania ogromnego konstruktora klasy Burger, jak to wygląda w praktyce?

Tworzymy klasę BurgerBuilder, która posiada identyczne atrybuty względem klasy Burger, dodatkowo definiujemy konstruktor, który tak jak w oryginale, wymaga zdefiniowania rozmiaru i typu mięsa. Główna różnica polega na dodaniu do klasy BurgerBuilder swoistych setterów, które mają za zadanie ustawić wartość konkretnego atrybutu w obiekcie, oto cała klasa:

<?php
class BurgerBuilder 
{
    public string $size;
    public string $meat;
    public bool $cheese = false;
    public bool $pepperoni = false;
    public bool $lettuce = false;
    public bool $tomato = false;
    public array $sauces = [];

    // Wymagamy podstawowych składników już w konstruktorze budowniczego
    public function __construct(string $size, string $meat) 
    {
        $this->size = $size;
        $this->meat = $meat;
    }

    public function addCheese(): self 
    {
        $this->cheese = true;
        return $this;
    }

    public function addPepperoni(): self 
    {
        $this->pepperoni = true;
        return $this;
    }

    public function addVegetables(): self 
    {
        $this->lettuce = true;
        $this->tomato = true;
        return $this;
    }

    public function addSauce(string $sauceName): self 
    {
        $this->sauces[] = $sauceName;
        return $this;
    }

    // Najważniejsza metoda – to ona "składa" gotowy produkt
    public function build(): Burger 
    {
        return new Burger($this);
    }
}

Znajdująca się na samym końcu metoda build() tworzy naszego burgera, ale o tym za chwilę, przyjrzyjmy się zmodyfikowanej klasie Burger:

<?php

class Burger 
{
    private string $size;
    private string $meat;
    private bool $cheese = false;
    private bool $pepperoni = false;
    private bool $lettuce = false;
    private bool $tomato = false;
    private array $sauces = [];

    // Konstruktor przyjmuje gotowy obiekt Budowniczego
    public function __construct(BurgerBuilder $builder) 
    {
        $this->size = $builder->size;
        $this->meat = $builder->meat;
        $this->cheese = $builder->cheese;
        $this->pepperoni = $builder->pepperoni;
        $this->lettuce = $builder->lettuce;
        $this->tomato = $builder->tomato;
        $this->sauces = $builder->sauces;
    }

    public function describe(): void 
    {
        echo "Burger ({$this->size}) z: {$this->meat}";
        if ($this->cheese) echo ", ser";
        if ($this->pepperoni) echo ", pepperoni";
        if ($this->lettuce) echo ", sałata";
        if ($this->tomato) echo ", pomidor";
        if (!empty($this->sauces)) echo ", sosy: " . implode(', ', $this->sauces);
        echo ".\n";
    }
}

Nasza modyfikacja polega na tym, że w konstruktorze klasy Burger nie definiujemy „na piechotę” pojedynczych parametrów, tylko przekazujemy mu cały obiekt klasy BurgerBuilder, który posiada skonfigurowany przez nas zestaw składników. Jak to wygląda w praktyce, stwórzmy od nowa nasze trzy przykłady burgerów, zacznijmy od prostego burgera:

$worldSimplestBurger = (new BurgerBuilder('XL', 'Wołowina'))->build();

Im bardziej skomplikowany burger, tym lepiej nasz Budowniczy sobie radzi, oto przykład wypasionego burgera:

$luxuryBurger = (new BurgerBuilder('L', 'Wołowina'))
    ->addCheese()
    ->addVegetables()
    ->addSauce('BBQ')
    ->addSauce('Czosnkowy')
    ->build();

Nasz kod zaczyna przypominać czytelny przepis kulinarny, jednak nadal pewnie to nie przekona niektórych, weźmy zatem ostatni przykład – ostrego mięśniaka:

$fireMeatBurger= (new BurgerBuilder('L', 'Kurczak'))
    ->addPepperoni()
    ->addSauce('Sriracha')
    ->build();

Ten przykład pokazuje piękno budowniczego, modyfikujemy wybrane atrybuty, na końcu wywołując metodę build(), która zwraca nam gotowego Burgera. Dodatkowo, jeśli w przyszłości dodamy nowe składniki, nie ma to wpływu na istniejące obiekty klasy Burger. jednak tutaj kryje się haczyk. Każdy nowy składnik w klasie Burger wymusza także rozbudowę klasy BurgerBuilder o ten sam atrybut, a także ustawienie odpowiedniego settera. Jeśli o tym zapomnimy, otrzymamy komunikat o błędzie.

Podsumowując, Budowniczy świetnie spisuje się, kiedy dysponujemy klasą z konstruktorem definiującym dużą ilość atrybutów, a nie zawsze chcemy modyfikować wszystkie wartości, a zależy nam na mocnej customizacji obiektu. Jeżeli chcemy tworzyć „hurtowo” gotowe obiekty bez wnikania w ich skład, bardziej powinniśmy postawić na wzorzec Fabryka. ale o tym opowiem innym razem 😉