Builder design pattern

Идеята е ако искаме да създаваме обекти от даден клас, които обаче могат да имат различни вариации. В смислъл, PhoneClass един път да има такива и такива свойства, друг път други.

Ако го направим с един клас PhoneClass, ще трябва на конструктора да му подадем всички възможни свойства. И съответно когато инстанцираме класа, да задаваме само нужните ни за дадената инстанция, а останалите да зададем с default стойност NULL, демек не винаги може да имаме нужда от всички, а може да са доста.
Което автоматично значи, че логиката в констуктурът се усложнява, става неелегантно, конструкторът става огромен и т.н…

Kакво можем да направим – дефинираме си един, така да се каже, общ за всички телефони клас, наречен BarePhoneBuilder, в който ще имаме всичко минимално (като методи, пропъртита и т.н.) за създаването на телефон. Но няма да създаваме обект от този клас, само ще го използваме чрез отделни негови методи и пропъртита (напр. сетъри за да му сетваме различните своиства динамично).

Идеята е да го използваме за да създаваме отделни видове телефони, имащи дадени общи, но и различни, специфични свойства, да викаме само част от методите, тези от които конкретно имаме нужда. Тук идва ролята на т.н. director (или manager) – това е друг клас, който задава какво конкретно трябва да притежава даденият конкретен модел телефон, извиквайки методи и пропъртита на гореспоменатият BarePhoneBuilder.
Тоест, director-ът е на ниво конкретен телефон.

Например, направи ми Galaxy A5!
Значи ще имаме клас GalaxyA5, който ще вика методи от BarePhoneBuilder, които ни трябват в този случай, само който му трябват за да създаде даденият конкретен модел Galaxy A5.

Тези методи ще са setters като им се подава конкретна за даденият модел стойност, те я сетват за дадения обект, може и допълнителна логика да извършват, и накрая връщат $this, за да можем за по-голямо удобство, да „чейнем на влакче“ отделните методи.

И воала – имаме Glaxy A5.

Искаме Nokia FS-12 – пак използваме билдъра BarePhoneBuilder, като „на влакче“ викаме отделните му методи, които ни трябват в този случай, сетваме му отделните им стойности и… имаме Nokia FS-12.

Така вече се вижда цялата структура на Builder design pattern – имаме Билдър клас (BarePhoneBuilder), който ползваме като общ за създаване изобщо на телефони.

Директорът използва от него само каквото му трябва за конкретният случай, задавайки желаните в конкретният случай свойства.

Директорът е на ниво конкретен обект, Билдърът – задава логиката, обща за всички обекти, като Директорът само му подава конкретните стойности.
Демек, „общата“ логика за създаване на конкретният обект е в Билдъра, Директорът само взема един „гол“ обект на Билдъра и и задава останалите свойства. Логиката, която е в Директора е по-скоро за неща като кога кое да бъде зададено, например ако искаме да имаме bluetooth support… и т.н….
Но как конкретно се прави „телефонът“ – в Билдъра.

Също, да не забравяме, че отделните пропърти стойности, които подаваме нa сетърите, могат и да са обекти, така идва и допълнителното определение за Builder design pattern – начин за гъвкаво създаване на обекти, съставени от други обекти (composition).

Kаква е задачата на директорите – да капсулират създаването на отделните конкретни обекти, за да става по-автоматизирано, и също – да скрият създаването от външният потребител.
Искаш Galaxy A5? Инстанцирай класът GalaxyA5 и готово, той си знае работата какво да сетне, какво да има това галакси като параметри и т.н… подробностите не те интересуват.

Естествено, за да можеш да „чейнваш на влакче“, първо трябва да имаш какво да чейнваш. Затова първо създаваш съвсем „гол“ обект, който да е „локомотивът“ и така…

Изводът, който се налага, е че Builder design pattern е удобен когато трябва да създаваме много близки по смисъл обекти, които могат да варират по отделните си свойства, и това би довело до експлозия от параметри в конструктора.

Да, може да има различни по-сложни варианти, например да можем да имаме много близки билдъри, които да наследяват общ абсклас или да имплементират общ интерфейс… но идеята остава същата.

Или обектите, които подаваме като аргументи на отделните сетъри, да екстендават общ абсклас или да имплементират общ интерфейс…

Един добър пример – с роботи:

<?php
/**
 * Задаваме какво задължително всеки робот трябва да има. Може с интерфейс,
 * може с абсклас.
 * Typically there's an abstract Builder class that defines an operation for each
 * component that a director may ask it to create. The operations do nothing by default.
 * A ConcreteBuilder (при нас - BareRobotBuilder) class overrides operations for
 * components it's interested in creating.
 */
interface BareRobotBuilderInterface
{
    public function setRobotHead(string $head) : void;
    public function setRobotTorso(string $torso) : void;
    public function setRobotArms(string $arms) : void;
    public function setRobotLegs(string $legs) : void;
}

/**
 * ...и го дефинираме, всъщност дефинираме методите,
 * които после Директорът ще използва за да създаде конкретен модел робот.
 */
class BareRobotBuilder implements BareRobotBuilderInterface
{
    private string $robotHead, $robotTorso, $robotArms, $robotLegs;

    public function setRobotHead(string $head) : void
    {
        $this->robotHead = $head;
    }

    public function setRobotTorso(string $torso) : void
    {
        $this->robotTorso = $torso;
    }

    public function setRobotArms(string $arms) : void
    {
        $this->robotArms = $arms;
    }

    public function setRobotLegs(string $legs) : void
    {
        $this->robotLegs = $legs;
    }
}

/**
 * Сега нека си зададем какво всеки Директор, който ще произвежда роботи,
 * трябва да може да прави.
 * Понеже идеята е да имаме много модели роботи, нека зададем общ интерфейс.
 */
interface RobotDirectorInterface
{
    public function buildRobotHead() : self;
    public function buildRobotTorso() : self;
    public function buildRobotArms() : self;
    public function buildRobotLegs() : self;
    public function getRobot() : BareRobotBuilderInterface;
}

/**
 * И да създадем един "директор", който ще задава как точно се прави
 * конкеретен модел роботи.
 */
class T1000RobotDirector implements RobotDirectorInterface
{
    public function __construct(
        private BareRobotBuilderInterface $robot
    ) { }

    public function buildRobotHead() : self
    {
        // щом имаме създаден/инджектнат "гол" робот, 
        // започваме да му дефинираме отделните части...
        $this->robot->setRobotHead('Някаква глава от течен метал бла бла…');
        return $this;
    }

    public function buildRobotTorso() : self
    {
        $this->robot->setRobotTorso('Някакво тяло от течен метал бла бла…');
        return $this;
    }

    public function buildRobotArms() : self
    {
        $this->robot->setRobotArms('Някакви ръце от течен метал бла бла…');
        return $this;
    }

    public function buildRobotLegs() : self
    {
        $this->robot->setRobotLegs('Някакви крака от течен метал бла бла…');
        return $this;
    }

    public function getRobot() : BareRobotBuilderInterface
    {
        return $this->robot;
    }
}

$t1000 = (new T1000RobotDirector(new BareRobotBuilder))
  ->buildRobotHead()  // може и да подаваме различните свойства на T-1000 с аргументи
  ->buildRobotTorso() // вместо да са хардкоднати в класа…
  //->buildRobotArms()
  ->buildRobotLegs();
var_dump($t1000->getRobot());

След като вече едно по едно сме задали отделните свойства на нашият робот, единият начин да го получим целият готов е да използваме директно getRobot() на T1000RobotDirector и от там да го върнем.
Нещо такова:

$t1000 = (new T1000RobotDirector(new BareRobotBuilder))
  ->buildRobotHead()  // може и да подаваме различните свойства на T-1000 с аргументи
  ->buildRobotTorso() // вместо да са хардкоднати в класа…
  //->buildRobotArms()
  ->buildRobotLegs();
var_dump($t1000->getRobot());

BareRobotBuilder е реално някакъв общ, абстрактен клас, където ще са само общите за всички роботи неща.
Вместо и така както е – интерфейс + клас, може и да е абстрактен клас например.

Излиза че самото създаване на конкретен робот става в T1000RobotDirector,
и то не точно крайното създаване, а само създаването на отделните части,
без самото сглобяване на целия робот.
Демек, само задава каква да е отделната част – това ще е такова, онова ще е онакова…

Builders construct their products in step-by-step fashion. Therefore the Builder class interface must be general enough to allow the construction of products for all kinds of concrete builders.
Демек Билдър интерфейса/абскласа трябва да е максимално универсален, но Директора вече решава дали да използва всичките му методи.

Друго удобство е, че можем и да не добавяме всички части, ако искаме например робот без ръце и т.н…

Builder design pattern напомня на Abstract factory design pattern но с разликата, че Builder създава обекта си „стъпка по стъпка“ и го връща готов в последната стъпка, а Abstract factory – наведнъж.

It gives you finer control over the construction process. Unlike creational
patterns that construct products in one shot, the Builder pattern constructs the
product step by step under the director’s control. Only when the product is
finished does the director retrieve it from the builder. Hence the Builder
interface reflects the process of constructing the product more than other
creational patterns. This gives you finer control over the construction process
and consequently the internal structure of the resulting product.

Демек, при Builder, създаването на обект е процес, не моментно събитие, което дава повече гъвкавост. Например може преди да добавиш свойство към BareRobotBuilder да искаш да провериш дали не е добавено вече, или дали не трябва нещо друго да е добавено…

Eто най-просто как стъпка по стъпка протича целият процес:
1) The client creates the Director object and configures it with the desired Builder
object.
2) Director notifies the builder whenever a part of the product should be built.
3) Builder handles requests from the director and adds parts to the product.
4) The client retrieves the product from the builder.

Времето тече „от горе надолу“