Keyvan Akbary Seguir

Keyvan Akbary

Tech Lead en TransferWise

Autor del libro

Saber más

Patrón Builder

El patrón builder entra dentro de la categoría de patrones de creación. Esto significa que su uso esta ideado para construir objetos. La idea primigenia plasmada en el ya clásico Gang of Four, gira entorno a desacoplar el código de construcción del código de representación.

Abstrae el proceso de creación de un objeto complejo, centralizando dicho proceso en un único punto, de tal forma que el mismo proceso de construcción pueda crear representaciones diferentes.

Las clases internas que participan en la construcción del objeto no forman parte del api público del Builder. El cliente no tiene por qué saber los detalles de cómo construir un objeto complejo. El uso de este patrón también alivia la congestión de métodos con muchos parámetros.

Por ejemplo, si disponemos de un objeto o producto cuya construcción es relativamente compleja, como una abstracta y deliciosa hamburguesa

class Burger {
    private $patty;
    private $toppings = [];
    private $bun;

    public function setBun(string $bun): void {
        $this->bun = $bun;
    }

    public function setPatty(string $patty): void {
        $this->patty = $patty;
    }

    public function addToppings(array $toppings): void {
        $this->toppings = $toppings;
    }
}

Y necesitamos cocinarla de diferentes maneras según la receta; podemos crear un abstract Builder que se especialize según la receta con implementaciones concretas haciendo uso del patrón template method

abstract class BurgerBuilder {
    protected $burger;

    public function createBurger(): void {
        $this->burger = new Burger();
    }

    public function getBurger(): Burger {
        return $this->burger;
    }

    abstract public function prepareBun(): void;
    abstract public function cookPatty(): void;
    abstract public function putToppings(): void;
}

Como una hamburgesa vegetariana

class VeggieBurgerBuilder extends BurgerBuilder {
    public function prepareBun(): void {
        $this->burger->setBun('brioche');
    }

    public function cookPatty(): void {
        $this->burger->setPatty('halloumi');
    }

    public function putToppings(): void {
        $this->burger->addToppings(['cauliflower', 'tomato', 'onion', 'cheese']);
    }
}

O una americana…

class AmericanBurgerBuilder extends BurgerBuilder {
    public function prepareBun(): void {
        $this->burger->setBun('slider');
    }

    public function cookPatty(): void {
        $this->burger->setPatty('beef');
    }

    public function putToppings(): void {
        $this->burger->addToppings(['tomato', 'cheese', 'onion', 'pickles', 'bacon']);
    }
}

El director, es decir, el chef, controla y gestiona de forma precisa el proceso de creación del producto

class BurgerChef {
    public function makeBurger(BurgerBuilder $builder): Burger {
        $builder->createBurger();
        $builder->prepareBun();
        $builder->cookPatty();
        $builder->putToppings();

        return $builder->getBurger();
    }
}

El cliente queda entonces liberado de detalles de construcción

$chef = new BurgerChef();
$vegieBurger = $chef->makeBurger(new VeggieBurgerBuilder());
$americanBurger = $chef->makeBurger(new AmericanBurgerBuilder());

Constructor Telescópico

Un problema especialmente conocido en lenguajes con sobrecarga de métodos como Java, C# o C++ es el famoso efecto del constructor telescópico. En PHP no podemos sobrecargar métodos pero si podemos entender el problema si evitamos pasar argumentos opcionales al constructor a base de añadir factory methods. Añadir argumentos al constructor provoca un incremento exponencial en la definición de métodos de inicialización.

class User {
    private $username;
    private $password;
    private $email;
    private $name;

    public function __construct(
        string $username,
        string $password,
        string $email = '',
        string $name = ''
    ) {
        $this->username = $username;
        $this->password = $password;
        $this->email = $email;
        $this->name = $name;
    }

    public static function create(string $username, string $password) {
        return new self($username, $password);
    }

    public static function createWithEmail(
        string $username,
        string $password,
        string $email
    ) {
        return new self($username, $password, $email);
    }

    public static function createWithName(
        string $username,
        string $password,
        string $name
    ) {
        return new self($username, $password, '', $name);
    }

    public static function createWithEmailAndName(
        string $username,
        string $password,
        string $email,
        string $name
    ) {
        return new self($username, $password, $email, $name);
    }
}

Añadir más argumentos al constructor incrementa el problema exponencialmente. Delegando en un Builder la construcción de User y haciendo uso de interfaz fluida aliviamos enormemente la complejidad del sistema. El trade-off es que exponemos al constructor del objeto que construyamos para que sea visible desde el Builder.

class UserBuilder {
    private $username;
    private $password;
    private $email = '';
    private $name = '';

    private function __construct(string $username, string $password) {
        $this->username = $username;
        $this->password = $password;
    }

    public static function aUser(string $username, string $password): self {
        return new self($username, $password);
    }

    public function withName(string $name): self {
        $this->name = $name;

        return $this;
    }

    public function withEmail(string $email): self {
        $this->email = $email;

        return $this;
    }

    public function build(): User {
        return new User($this->username, $this->password, $this->email, $this->name);
    }
}

Crear un User sin nombre ni email es tan sencillo como

$user = UserBuilder::aUser('keyvan', 'pass')->build();

De la misma forma, añadir los parámetros opcionales es tan fácil como

$user = UserBuilder::aUser('keyvan', 'pass')
    ->withName('Keyvan Akbary')
    ->withEmail('keyvan@example.com')
    ->build();

Referencias

¿Ves algo raro? ¡Editame!

Copyright © 2022