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 😉

