Builder (Budowniczy)

builder budowniczy uml

Opis

Wzorzec projektowy Builder (Budowniczy) należy do grupy wzorców konstrukcyjnych. Polega na konstruowaniu obiektów z mniejszych elementów w wielu możliwych konfiguracjach. Każdy z tych etapów może być implementowany w różny sposób, dzięki czemu możliwe jest uzyskanie wielu reprezentacji klasy wykorzystując ten sam proces konstrukcyjny.

Wzorzec Builder jest dedykowany raczej bardziej skomplikowanym obiektom – a nawet nie tyle obiektom, co skomplikowanym procesom ich tworzenia. Przykładów nie trzeba daleko szukać i jeśli zaznajomieni jesteście z Doctrine ORM to na pewno mieliście okazje skorzystać z mechanizmu budowania zapytania używając Query Buildera. To świetne miejsce na jego wykorzystanie. W końcu komendy SQL mogą przyjąć różną formę, natomiast wiele z kroków budowania zapytania musi być użyte w odpowiedniej sekwencji (where() nigdy nie wystąpi przed select()).

Problem i rozwiązanie

Wyobraźcie sobie, że klasa może mieć wiele różnych reprezentacji obiektu złożonego z dużej liczby atrybutów i zależnego od innych obiektów. Zwykle skończy się to na ogromnym konstruktorze z wieloma parametrami, co gorsza część z nich pewnie opcjonalna skoro obiekt może być zbudowany na kilka sposobów. Druga, jeszcze gorsza opcja to brak kontraktu w konstruktorze i przeniesienie balastu tworzenia obiektu do kodu klienckiego – czyli klasyczne settery.

Niektóre z tych przypadków dodatkowo wymagają spójności obiektu zanim zostaną wykonane na nim kolejne operacje. O co chodzi? Jeżeli klient będzie musiał zbudować dany obiekt, to może pominąć jakiś krok. Nie ustawić wymaganego parametru albo skonstruować bezsensowną reprezentację klasy. Tym samym powodując błąd w działaniu aplikacji, bo nie zbudujemy domu z dwóch ścian. Nie wspominając już o sytuacji, gdzie w wielu miejscach trzeba dostarczyć tak samo skonfigurowany obiekt i duplikować kod. Jasne wszystko da się ifologią załatwić – tylko po co się męczyć?

Wtedy do gry wchodzi Budowniczy, który dostarczy interfejs do budowania obiektu w pewnej sekwencji kroków. Klient nie musi się trudzić w jego konfigurowanie, a dokładniej to nawet nie ma pojęcia w jaki sposób się to odbywa, bo korzysta z odpowiedniej abstrakcji. Chyba, że celowo mu to umożliwimy jak właśnie w doctrinowej implementacji, gdzie sposób budowania zapytania SQL prawie zawsze jest inny. Tam nie występuje klasa Dyrektora w Builderze, której odpowiedzialnością jest dostarczenie w pełni zbudowanego obiektu.

Wszystko zależy więc od problemu. Nadzorca daje jednak wiele korzyści, bo świetnie enkapsuluje logikę i powoduje, że kod jest reużywalny. Jeżeli zostanie on wprowadzony do mechanizmu to można zauważyć, że struktura klas przypomina nieco fabrykę abstrakcyjną. W zamyśle wzorce te różnią się jednak celem. Abstract Factory zwraca obiekt bezzwłocznie i skupia się tylko na jego kreacji. Builder poza jego stworzeniem pozwala dobudować do niego kolejne fragmenty.

Plusy i minusy

Jeżeli chodzi o dużą liczbę dodatkowych klas to wydaje mi się, że wada występuję w większości wzorców OOP. Dobrze poukładana struktura katalogów i klas gwarantuje jednak odpowiednią czytelność, a małe klasy są dużo łatwiejsze w zarządzaniu. Łatwo więc tę wadę przekuć w zaletę.

Kolejnym minusem jest fakt, że przy procesie budowania obiektu trochę więcej wiedzy domenowej trzeba przerzucić do budowniczych. W większości przypadków nie ma jednak problemu, żeby odpowiednio postawić granicę i dobrze podzielić kod. Trzeba mieć jednak tego świadomość, żeby za bardzo nie poplątać warstw.

Ostatnią wadą jaka przychodzi mi do głowy to brak wykorzystania DI (Dependency Injection). Prowadzi chociażby do kodu trudniejszego w testowaniu. Zawsze można wstrzyknąć konkretnego budowniczego do kodu klienckiego poprzez ogólny interfejs. Ma to jednak sens tylko wtedy, gdy akurat wykorzystujemy jednego konkretnego Buildera. Fakt faktem, że utworzenie Budowniczego przez new jest proste i kuszące, bo zazwyczaj nie wymaga dodatkowych zależności.

Jak to już z tymi wzorcami – zalet jest znacznie więcej! Przede wszystkim mechanizm ten jest łatwy w rozszerzaniu i skalowalny, a każda klasa ma dokładnie jedną odpowiedzialność. Jeżeli wykorzystana zostanie klasa Director to zapewniona jest również reużywalność i odpowiednia enkapsulacja.

Wykorzystanie wzorca projektowego Budowniczy jest również zabiegiem zwiększającym czytelność. Bardzo łatwo jest zrozumieć z jakich elementów zbudowany jest obiekt, kiedy każda metoda odpowiada za jeden krok i jest właściwie nazwana. Warto także wspomnieć o niezawodności tej implementacji. Mam tutaj na myśli odpowiedzialność nadzorcy, czyli dostarczenie reprezentacji w pełni gotowej do wykorzystania.

Przykładowa implementacja w PHP

Wiem, że we wstępie zastrzegałem się, że wzorzec projektowy Budowniczy służy do konstruowania skomplikowanych obiektów. Nie wycofuję się z tego, a poniższy przykład wcale nie ma temu zaprzeczać. Wszelkie przykłady, które prezentuję w tej serii są koncepcyjnie uproszczone. Naturalnie 3 metody dostarczone przez interfejs umowy w prawdziwym życiu nie będą wystarczająco opisywały obiektu. Oczywiście parametr $contactDetails nie powinien być stringiem, a obiektem. I tak dalej…

Przykłady mają jednak skupiać się na samym mechanizmie i działaniu patternu. Tego nigdy nie upraszczam w swoich implementacjach. Zapewniam, że jeżeli zrozumiecie jak działa poniższa implementacja, to nie powinniście mieć problemu z dodaniem kolejnego kroku do procesu budowania obiektu.

Zanim przejdziecie do kodu to Jeszcze jedna uwaga. W interfejsie budowniczego nie zawsze da się umieścić metodę pozwalającą na pobranie obiektu (w moim przypadku getAgreement()). U mnie akurat klasy produktów łączy wspólny interfejs, ale nie zawsze tak będzie. Nie ma się co tym martwić, ale trzeba zawsze pamiętać żeby taką metodę zaimplementować, dlatego jeśli jest możliwość to należy to wymusić.

<?php

declare(strict_types=1);

namespace DesignPatterns\Creational\Builder;

abstract class AbstractAgreement
{
    private string $title;

    private int $salary;

    private string $contactDetails;

    public function setTitle(string $title): void
    {
        $this->title = $title;
    }

    public function setSalary(int $salary): void
    {
        $this->salary = $salary;
    }

    public function setContactDetails(string $contactDetails): void
    {
        $this->contactDetails = $contactDetails;
    }
}

<?php

declare(strict_types=1);

namespace DesignPatterns\Creational\Builder;

final class B2BContract extends AbstractAgreement
{
}
<?php

declare(strict_types=1);

namespace DesignPatterns\Creational\Builder;

final class EmploymentContract extends AbstractAgreement
{
}
<?php

declare(strict_types=1);

namespace DesignPatterns\Creational\Builder;

interface AgreementBuilderInterface
{
    public function addHeader(): AgreementBuilderInterface;

    public function addSalary(int $salary): AgreementBuilderInterface;

    public function addContactDetails(string $contactDetails): AgreementBuilderInterface;

    public function getAgreement(): AbstractAgreement;
}

<?php

declare(strict_types=1);

namespace DesignPatterns\Creational\Builder;

final class B2BContractBuilder implements AgreementBuilderInterface
{
    private AbstractAgreement $b2BContract;

    public function __construct()
    {
        $this->b2BContract = new B2BContract();
    }

    public function addHeader(): AgreementBuilderInterface
    {
        $this->b2BContract->setTitle('Services Contract');

        return $this;
    }

    public function addSalary(int $salary): AgreementBuilderInterface
    {
        $this->b2BContract->setSalary($salary);

        return $this;
    }

    public function addContactDetails(string $contactDetails): AgreementBuilderInterface
    {
        $this->b2BContract->setContactDetails(sprintf('Company: %s', $contactDetails));

        return $this;
    }

    public function getAgreement(): AbstractAgreement
    {
        return $this->b2BContract;
    }
}
<?php

declare(strict_types=1);

namespace DesignPatterns\Creational\Builder;

final class EmploymentContractBuilder implements AgreementBuilderInterface
{
    private AbstractAgreement $employmentContract;

    public function __construct()
    {
        $this->employmentContract = new EmploymentContract();
    }

    public function addHeader(): AgreementBuilderInterface
    {
        $this->employmentContract->setTitle('Employment Contract');

        return $this;
    }

    public function addSalary(int $salary): AgreementBuilderInterface
    {
        $this->employmentContract->setSalary($salary);

        return $this;
    }

    public function addContactDetails(string $contactDetails): AgreementBuilderInterface
    {
        $this->employmentContract->setContactDetails(sprintf('Person: %s', $contactDetails));

        return $this;
    }

    public function getAgreement(): AbstractAgreement
    {
        return $this->employmentContract;
    }
}
<?php

declare(strict_types=1);

namespace DesignPatterns\Creational\Builder;

final class AgreementDirector
{
    private AgreementBuilderInterface $builder;

    public function __construct(AgreementBuilderInterface $builder)
    {
        $this->builder = $builder;
    }

    public function changeBuilder(AgreementBuilderInterface $builder): void
    {
        $this->builder = $builder;
    }

    public function buildAnonymousAgreement(int $salary): void
    {
        $this->builder
            ->addHeader()
            ->addSalary($salary);
    }

    public function buildFullDetailsAgreement(int $salary, string $contactDetails): void
    {
        $this->builder
            ->addHeader()
            ->addSalary($salary)
            ->addContactDetails($contactDetails);
    }
}
<?php

declare(strict_types=1);

namespace DesignPatterns\Creational\Builder\Test;

use DesignPatterns\Creational\Builder\AgreementDirector;
use DesignPatterns\Creational\Builder\B2BContract;
use DesignPatterns\Creational\Builder\B2BContractBuilder;
use DesignPatterns\Creational\Builder\EmploymentContract;
use DesignPatterns\Creational\Builder\EmploymentContractBuilder;
use PHPUnit\Framework\TestCase;

final class AgreementDirectorTest extends TestCase
{
    public function testCanBuildAnonymousB2BContract(): void
    {
        $b2bContractBuilder = new B2BContractBuilder();
        $director = new AgreementDirector($b2bContractBuilder);

        $director->buildAnonymousAgreement(6000);

        self::assertInstanceOf(B2BContract::class, $b2bContractBuilder->getAgreement());
    }

    public function testCanBuildFullDetailsB2BContract(): void
    {
        $b2bContractBuilder = new B2BContractBuilder();
        $director = new AgreementDirector($b2bContractBuilder);

        $director->buildFullDetailsAgreement(6000, 'Test Company Ltc.');

        self::assertInstanceOf(B2BContract::class, $b2bContractBuilder->getAgreement());
    }

    public function testCanBuildAnonymousEmploymentContract(): void
    {
        $employmentContractBuilder = new EmploymentContractBuilder();
        $director = new AgreementDirector($employmentContractBuilder);

        $director->buildAnonymousAgreement(5000);

        self::assertInstanceOf(EmploymentContract::class, $employmentContractBuilder->getAgreement());
    }

    public function testCanBuildFullDetailsEmploymentContract(): void
    {
        $employmentContractBuilder = new EmploymentContractBuilder();
        $director = new AgreementDirector($employmentContractBuilder);

        $director->buildFullDetailsAgreement(5000, 'Jane Doe');

        self::assertInstanceOf(EmploymentContract::class, $employmentContractBuilder->getAgreement());
    }

    public function testCanSwitchBuilder(): void
    {
        $b2bContractBuilder = new B2BContractBuilder();
        $director = new AgreementDirector($b2bContractBuilder);
        $director->buildAnonymousAgreement(6000);
        $employmentContractBuilder = new EmploymentContractBuilder();

        $director->changeBuilder($employmentContractBuilder);
        $director->buildAnonymousAgreement(5000);

        self::assertInstanceOf(EmploymentContract::class, $employmentContractBuilder->getAgreement());
    }
}

Builder – podsumowanie

Tak jak już podkreślałem – Builder jest bardzo czytelnym rozwiązaniem. Gdy istnieje potrzeba sprawdzenia z czego zbudowany jest obiekt, wystarczy zajrzeć do procesu jego konstrukcji. Dołożenie kolejnego elementu do układanki także jest przyjemnością.

Nie jest łatwo rozpoznać odpowiednie miejsce do zaimplementowania tego wzorca. Tak samo jak fabrykę można stworzyć dla każdego obiektu, tak samo każdy obiekt trzeba zbudować, więc można pokusić się o budowniczego. Nie ma to jednak sensu, kiedy więcej zachodu niż korzyści.

Krystian Żądło
Programista PHP i właściciel marki Koddlo. Pasjonat czystego kodu i dobrych praktyk programowania obiektowego. Prywatnie fan angielskiej piłki nożnej, dobrego humoru oraz podcastów.