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 raczej dedykowany bardziej skomplikowanym obiektom. 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 gorsze, część z nich jest 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ąć jeden z kroków. Nie ustawi wymaganego parametru albo skonstruuje bezsensowną reprezentację klasy, tym samym powodując błąd w działaniu aplikacji. Nie da się zbudować 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ć jego konfigurowaniem, a dokładniej to nawet nie ma pojęcia w jaki sposób się to odbywa. Korzysta on z odpowiedniej abstrakcji. Chyba, że celowo zostanie mu to umożliwione. Tak jak chociażby 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.

Nadzorca daje 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ę 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 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 wzorca DI (dependency injection). Prowadzi chociażby do trudniejszego w testowaniu kodu. Zawsze można wstrzyknąć konkretnego budowniczego do kodu klienckiego poprzez ogólny interfejs. Ma to jednak sens tylko wtedy, gdy akurat wykorzystywany jest konkretny Builder. Fakt faktem, że utworzenie Budowniczego przez new jest proste i kuszące, bo zazwyczaj nie wymaga on dodatkowych zależności.

Zalet jak zwykle jest znacznie więcej! Przede wszystkim, mechanizm ten jest skalowalny i łatwy w rozszerzaniu, a każda klasa ma dokładnie jedną odpowiedzialność (single responsibility). Jeżeli wykorzystana zostanie klasa Director to zapewniona jest też 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 charakteryzował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ć.

Kod pozwala zbudować dwa rodzaje umów: B2B i umowa o pracę. Klasa AgreementDirector dostarcza kilka gotowych metod budowania takich dokumentów. Dołożenie kolejnych implementacji budowniczego dla nowych typów dokumentów, czy dostarczenie innych gotowych sposóbów tworzenia nie powinno być problematyczne.

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

Jego wykorzystanie może się szybko zwrócić podczas rozwoju aplikacji, kiedy dojdzie nowy typ produktu albo kolejna metoda umożliwiająca rozbudowę obiektu. Zdecydowanie wolę opcję z dyrektorem, ale nie zawsze jest on potrzebny.

Programista PHP i właściciel marki Koddlo. Pasjonat czystego kodu i dobrych praktyk programowania obiektowego. Prywatnie fan dobrego humoru i podcastów.